clickroom 0.3.4 → 0.3.5

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/cli/push.js CHANGED
@@ -7,6 +7,7 @@ import { BACKUP_DIR, copyBuild, cloudUrl, buildPages, baseUrl, localUrl, isTeamm
7
7
  import { call } from './client.js';
8
8
  import { dim } from './dev.js';
9
9
  import { rememberFolder } from './changes.js';
10
+ import { serversInProject, probe } from './detect.js';
10
11
 
11
12
  const OUT_DIRS = ['dist', 'build', 'out', '.output/public'];
12
13
 
@@ -98,11 +99,16 @@ export async function push(name, local, { note, live = false, build, out, quiet
98
99
  } else {
99
100
  const pkgDir = packageDir(dir, local.cwd);
100
101
  const cmd = command || buildScript(pkgDir);
101
- if (!cmd) throw new NoSnapshot(`No build script in ${path.join(pkgDir || dir, 'package.json')}, so there's nothing to snapshot.`);
102
- const started = Date.now();
103
- if (!quiet) console.log(dim(` Building v${next}: ${cmd}`));
104
- await run(cmd, pkgDir || dir);
105
- source = out || local.out ? path.resolve(pkgDir || dir, out || local.out) : findOutput(pkgDir || dir, started);
102
+ // No build step: a folder of plain web files (served by any little server) is its own snapshot.
103
+ const plain = !cmd && [pkgDir || dir, ...OUT_DIRS.map((d) => path.join(pkgDir || dir, d))].find((d) => fs.existsSync(path.join(d, 'index.html')));
104
+ if (plain) source = plain;
105
+ else if (!cmd) throw new NoSnapshot(`No build script in ${path.join(pkgDir || dir, 'package.json')}, so there's nothing to snapshot.`);
106
+ else {
107
+ const started = Date.now();
108
+ if (!quiet) console.log(dim(` Building v${next}: ${cmd}`));
109
+ await run(cmd, pkgDir || dir);
110
+ source = out || local.out ? path.resolve(pkgDir || dir, out || local.out) : findOutput(pkgDir || dir, started);
111
+ }
106
112
  if (!source) throw new NoSnapshot(`The build finished, but no folder with an index.html was produced (looked in ${OUT_DIRS.join(', ')}).`);
107
113
  }
108
114
  if (!fs.existsSync(path.join(source, 'index.html'))) throw new NoSnapshot(`${source} has no index.html.`);
@@ -213,6 +219,59 @@ export async function backup(name) {
213
219
  }
214
220
 
215
221
  // Nearest folder with a package.json, from where clickroom was run up to the project root.
222
+ // No dev server running (the owner's Live view): start the project's own (`npm run dev`, else
223
+ // `start`) for as long as the session runs. { target, how, stop } once it answers in this project,
224
+ // null without such a script.
225
+ export async function startDevServer(root, { exclude = [] } = {}) {
226
+ const dir = packageDir(root, root);
227
+ if (!dir) return null;
228
+ let pkg;
229
+ try {
230
+ pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
231
+ } catch {
232
+ return null;
233
+ }
234
+ const script = ['dev', 'start'].find((s) => pkg.scripts?.[s]);
235
+ if (!script) return null;
236
+ const pm = packageManager(dir);
237
+ const cmd = `${pm} run ${script}`;
238
+ if (Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).length && !fs.existsSync(path.join(dir, 'node_modules'))) {
239
+ console.log(dim(` Installing the project's packages (${pm} install; the first time only, this can take a minute)…`));
240
+ await run(`${pm} install`, dir).catch((err) => {
241
+ throw new FolderProblem(`Installing the project's packages failed.\n${err.message.replace(/^.*\n/, '')}`, `${pm} install fails in this project; please fix it, then run npx clickroom again.`);
242
+ });
243
+ }
244
+ console.log(dim(` No dev server running, so clickroom starts it: ${cmd}`));
245
+ // Its own process group, so stopping it also stops what it started (vite, next, …).
246
+ const child = spawn(cmd, { cwd: dir, shell: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, BROWSER: 'none', FORCE_COLOR: '0' } });
247
+ let log = '';
248
+ const keep = (d) => (log = (log + d).slice(-4000));
249
+ child.stdout.on('data', keep);
250
+ child.stderr.on('data', keep);
251
+ let exited = false;
252
+ child.on('exit', () => (exited = true));
253
+ child.on('error', () => (exited = true));
254
+ const stop = () => {
255
+ try {
256
+ if (process.platform === 'win32') child.kill();
257
+ else process.kill(-child.pid, 'SIGTERM');
258
+ } catch {}
259
+ };
260
+ for (let i = 0; i < 90 && !exited; i++) {
261
+ await new Promise((r) => setTimeout(r, 1000));
262
+ const own = await serversInProject(root, { exclude });
263
+ for (const s of own) {
264
+ const p = await probe(s.port);
265
+ if (p.ok && p.html) return { target: `http://localhost:${s.port}`, how: `${cmd}, started by clickroom`, stop };
266
+ }
267
+ }
268
+ stop();
269
+ throw new FolderProblem(
270
+ `${exited ? `\`${cmd}\` stopped` : `\`${cmd}\` didn't start a web server within 90 seconds`}:\n${log.trim().split('\n').slice(-8).map((l) => ' ' + l).join('\n')}`,
271
+ `\`${cmd}\` doesn't start the dev server in this project; please fix it, then run npx clickroom again.`,
272
+ );
273
+ }
274
+
216
275
  function packageDir(dir, root) {
217
276
  let d = path.resolve(dir);
218
277
  const stop = path.resolve(root || dir);
package/cli/share.js CHANGED
@@ -6,7 +6,7 @@ import { readLocal, writeLocal, copyBuild, cloudUrl, baseUrl, isTeammate, adminT
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';
9
- import { push, pushHint, backup, NoSnapshot, FolderProblem, prepareFolder, projectFolder } from './push.js';
9
+ import { push, pushHint, backup, NoSnapshot, FolderProblem, prepareFolder, projectFolder, startDevServer } from './push.js';
10
10
  import { folderChanges } from './changes.js';
11
11
  import { connectAgents, agentsReport } from './agents.js';
12
12
  import { autoNote } from './note.js';
@@ -57,6 +57,17 @@ export async function share(arg, opts) {
57
57
  const what = await prepareFolder(cwd, root).catch(explain);
58
58
  target = what.kind === 'static' ? { kind: 'static', target: null, source: what.source, how: 'the files as they are' } : { kind: 'build', target: null, source: null, how: 'each push runs its build' };
59
59
  }
60
+ // Nothing running in this project: clickroom starts the project's own dev server (until q), and
61
+ // a folder of plain web files needs none: its files are the versions.
62
+ let dev = null;
63
+ if (!teammate && !arg && (!target || target.uncertain)) {
64
+ dev = await startDevServer(root, { exclude: [port, 9229, 9230] }).catch(explain);
65
+ if (dev) {
66
+ target = { kind: 'url', target: dev.target, how: dev.how };
67
+ process.on('exit', dev.stop); // also when the session ends with an error
68
+ }
69
+ else if (fs.existsSync(path.join(root, 'index.html'))) target = { kind: 'static', target: null, source: root, how: 'the files as they are' };
70
+ }
60
71
  if (!target) {
61
72
  throw new Error(
62
73
  'No running dev server found for this project.\n' +
@@ -79,8 +90,17 @@ export async function share(arg, opts) {
79
90
  console.log(dim(` Sharing through ${cloud} as you`));
80
91
  } else if (cloud) {
81
92
  console.log(dim(` Sharing through ${cloud}`));
93
+ // The Live view reaches this Mac through the local clickroom server (the relay). Not running
94
+ // (no `clickroom service`): this session runs it, until q.
82
95
  const relay = await fetch(`http://localhost:${port}/__ps/api/health`).then((r) => r.ok, () => false);
83
- if (!relay) console.log(dim(' The local clickroom server is not running, so the Live view and app API calls are offline.\n Start it in the background with `clickroom service install`.'));
96
+ if (!relay && target.kind === 'url') {
97
+ console.log(dim(' Starting the local clickroom server for the Live view…'));
98
+ server = await startServer({ ...opts, port, tunnel: true }).catch((e) => {
99
+ console.log(dim(` Couldn't start it (${e.message}), so the Live view is offline; pushed versions work.`));
100
+ return null;
101
+ });
102
+ delete process.env.CLICKROOM_URL; // startServer points the CLI at it; this session talks to the cloud
103
+ }
84
104
  } else if (await isUp()) {
85
105
  console.log(dim(` Attaching to the clickroom server already running on :${port}`));
86
106
  } else {
@@ -253,10 +273,11 @@ export async function share(arg, opts) {
253
273
  watcher?.close();
254
274
  await call('DELETE', `/__ps/api/projects/${encodeURIComponent(name)}/session`).catch(() => {});
255
275
  await backup(name);
276
+ dev?.stop();
256
277
  if (server) {
257
278
  server.stop();
258
279
  await Promise.race([server.exited, new Promise((r) => setTimeout(r, 3000))]);
259
- console.log(dim('\n Tunnel closed.'));
280
+ console.log(dim(cloud ? '\n Live view stopped (pushed versions stay online).' : '\n Tunnel closed.'));
260
281
  }
261
282
  process.exit(0);
262
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clickroom",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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": {
@@ -13,6 +13,7 @@
13
13
  "test": "node --test test/*.test.js",
14
14
  "screens": "node examples/screens/build.js",
15
15
  "demo:seed": "node examples/demo/seed.js",
16
+ "release": "npm version patch --no-git-tag-version && npm publish && git commit -am \"$(node -p 'require(\"./package.json\").version')\"",
16
17
  "prepublishOnly": "node --test test/detect.test.js test/changes.test.js test/webpush.test.js"
17
18
  },
18
19
  "engines": {