clickroom 0.1.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
@@ -98,6 +98,28 @@ Each `clickroom` terminal is a live **session**. The dashboard (`/`, team only)
98
98
 
99
99
  In a review, the project name in the toolbar is a switcher with the same list. `clickroom list` prints it in the terminal.
100
100
 
101
+ ## Workspaces
102
+
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
+
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.
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.
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.
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.
115
+
116
+ How it's set up:
117
+ - `APP_HOST` (`{ws}.clickroom.dev`) and `DEFAULT_WS` (`yna`) turn workspaces on. The first workspace keeps its storage from before workspaces (Registry `registry`, rooms and builds by project name). Others use Registry `ws:<slug>`, and rooms and builds under `<slug>/`.
118
+ - The `Directory` Durable Object knows every workspace and which email addresses are in each. Each workspace's Registry keeps that list up to date.
119
+ - The bare domain (`AUTH_URL`) has the public pages, `/login` and the list of your workspaces. It also keeps the API for CLIs set up before, and Google's return address.
120
+ - Prototypes run on `{name}--{ws}.clickroom-preview.dev` (`PROTO_HOST`), a separate site. Each review address frames the review from there.
121
+ - `PASSTHROUGH_HOSTS` lists hosts under the wildcard route that aren't the app's, such as the relay's tunnel.
122
+
101
123
  ## People
102
124
 
103
125
  Everyone except share-link clients signs in as themselves, by email address. A browser stays signed in for 30 days from its last visit; signing in again while signed in (clickroom.dev's *Sign in*) goes straight to the dashboard.
@@ -130,12 +152,12 @@ Teammates share their own builds with you through your clickroom (for example cl
130
152
 
131
153
  ```sh
132
154
  npm install -g clickroom
133
- clickroom login # opens clickroom in the browser: approve this computer
155
+ clickroom login # opens clickroom in the browser: sign in, approve this computer
134
156
  cd ~/code/their-app
135
157
  clickroom # builds it, uploads it as v1; p pushes the next version
136
158
  ```
137
159
 
138
- - **`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.
139
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.
140
162
  - `clickroom logout` ends it. *Revoke* on the People panel ends it as well, along with their browser sign-ins.
141
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 [url] connect this computer to your team's clickroom (default https://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/gaps.js CHANGED
@@ -18,7 +18,7 @@ const questionCount = async (name) =>
18
18
 
19
19
  // Resolves to the number of questions it added (drafts), or throws.
20
20
  export async function findGaps(name, root) {
21
- if (!hasClaude()) throw new Error('Claude Code (`claude`) is not installed on this Mac');
21
+ if (!hasClaude()) throw new Error('Claude Code (`claude`) is not installed on the computer running `clickroom` for this project');
22
22
  const before = await questionCount(name);
23
23
  const mcp = JSON.stringify({ mcpServers: { clickroom: { command: process.execPath, args: [path.join(ROOT, 'bin', 'clickroom.js'), 'mcp'] } } });
24
24
  const args = ['-p', gapsPrompt(name), '--mcp-config', mcp, '--strict-mcp-config', '--tools', 'Read,Glob,Grep', '--allowedTools', TOOLS.join(','), '--disallowedTools', NEVER.join(','), '--no-session-persistence'];
package/cli/login.js CHANGED
@@ -5,14 +5,47 @@ import { bold, dim } from './dev.js';
5
5
 
6
6
  export const DEFAULT_URL = 'https://clickroom.dev';
7
7
 
8
- // `clickroom login [url]`: connects this computer to a clickroom server as you. It opens a page in
9
- // your browser (where you're signed in from your invite) to approve this computer, then keeps a
10
- // key of its own in ~/.clickroom/credentials.json.
8
+ // `clickroom login [workspace|url]`: connects this computer to a clickroom workspace as you
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
+
11
17
  export async function login(arg) {
12
- const url = (arg || process.env.CLICKROOM_URL || credentials()?.url || DEFAULT_URL).replace(/\/$/, '');
13
- if (!/^https?:\/\//.test(url)) throw new Error('usage: clickroom login [https://clickroom.example]');
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(/\/$/, '');
24
+ if (!/^https?:\/\//.test(url)) throw new Error('usage: clickroom login <workspace> (or a URL: clickroom login https://acme.clickroom.dev)');
14
25
  if (adminToken() && !process.env.CLICKROOM_TOKEN) console.log(dim(' This computer has the admin key (.dev.vars), which the clickroom command uses first.'));
15
- 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, {
16
49
  method: 'POST',
17
50
  headers: { 'content-type': 'application/json' },
18
51
  body: JSON.stringify({ label: `${os.userInfo().username} on ${os.hostname().replace(/\.local$/, '')}` }),
@@ -20,16 +53,14 @@ export async function login(arg) {
20
53
  if (!res) throw new Error(`Can't reach ${url}.`);
21
54
  const start = await res.json().catch(() => ({}));
22
55
  if (!res.ok) throw new Error(start.error || `${url} answered ${res.status}`);
56
+ return start;
57
+ }
23
58
 
24
- console.log(`\n Approve this computer in your browser:\n ${bold(start.url)}\n`);
25
- console.log(` Check that the page shows ${bold(start.code)}.`);
26
- console.log(dim(' Not signed in there? Sign in with the email address you were invited with.\n'));
27
- openUrl(start.url);
28
-
59
+ async function wait(endpoint, start) {
29
60
  const until = Date.now() + start.expiresIn * 1000;
30
61
  while (Date.now() < until) {
31
62
  await new Promise((r) => setTimeout(r, 2000));
32
- const r = await fetch(`${url}/__ps/api/cli-login/poll`, {
63
+ const r = await fetch(endpoint, {
33
64
  method: 'POST',
34
65
  headers: { 'content-type': 'application/json' },
35
66
  body: JSON.stringify({ poll: start.poll }),
@@ -37,14 +68,14 @@ export async function login(arg) {
37
68
  if (!r?.ok) continue;
38
69
  const d = await r.json();
39
70
  if (d.status === 'expired') break;
40
- if (d.status === 'approved') {
41
- saveCredentials({ url, token: d.token, name: d.me.name, email: d.me.email || null, at: new Date().toISOString() });
42
- console.log(`${bold('✔')} Signed in to ${url} as ${bold(d.me.name)}${d.me.email ? ` (${d.me.email})` : ''}.`);
43
- console.log(dim(' Run `clickroom` in a project to share it.'));
44
- return;
45
- }
71
+ if (d.status === 'approved') return d;
46
72
  }
47
- 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})` : ''}.`);
48
79
  }
49
80
 
50
81
  export async function logout() {
package/cli/note.js CHANGED
@@ -3,7 +3,7 @@ import { call } from './client.js';
3
3
  import { changedSource, folderChanges } from './changes.js';
4
4
 
5
5
  // The version note when a push comes without one ("Push v16" on the dashboard, or an empty
6
- // answer in the terminal): Claude Code on this Mac (`claude -p`) describes the source changes
6
+ // answer in the terminal): Claude Code on this computer (`claude -p`) describes the source changes
7
7
  // since the last push as reviewers will notice them, plus the comments fixed in this version.
8
8
  // Without git, Claude or a usable answer: the fixed comments, else the changed file names.
9
9
  // CLICKROOM_AUTO_NOTE=off never asks Claude.
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}`;
@@ -102,8 +110,9 @@ export async function share(arg, opts) {
102
110
  let base, teamUrl, clientUrl;
103
111
  const links = () => {
104
112
  base = tunnelUrl || `http://localhost:${port}`;
105
- teamUrl = `${base}/s/${encodeURIComponent(name)}`;
106
- clientUrl = `${project.origin || base}/s/${encodeURIComponent(name)}?t=${project.token}`;
113
+ // The review's own address when the server has one (APP_URL/<name>), else /s/<name>.
114
+ teamUrl = project.reviewUrl || `${base}/s/${encodeURIComponent(name)}`;
115
+ clientUrl = `${project.reviewUrl || `${project.origin || base}/s/${encodeURIComponent(name)}`}?t=${project.token}`;
107
116
  console.log('');
108
117
  console.log(` ${bold('Review (you, team)')} ${teamUrl} ${dim('(press o to open it signed in)')}`);
109
118
  console.log(` ${bold('Client link')} ${clientUrl}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clickroom",
3
- "version": "0.1.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": {