@fapost/jig-ui 0.1.0-next.16 → 0.1.0-next.18

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/dist/main.js CHANGED
@@ -6,7 +6,8 @@ import { DEFAULT_PORT, parseCliArgs } from './args.js';
6
6
  import { createApp, readPackageVersion } from './http/app.js';
7
7
  import { openBrowser } from './open-browser.js';
8
8
  import { configDir } from './registry/config-dir.js';
9
- import { probeInstance, readInstance, removeInstance, writeInstance } from './registry/instance.js';
9
+ import { connectThrough, probeInstance, readInstance, removeInstance, writeInstance } from './registry/instance.js';
10
+ import { mainCheckout } from './source/checkout.js';
10
11
  import { localSource } from './source/local.js';
11
12
  const HOST = '127.0.0.1';
12
13
  const HELP = `Usage: jig-ui [options]
@@ -21,12 +22,40 @@ function fail(message) {
21
22
  console.error(message);
22
23
  process.exit(1);
23
24
  }
24
- const link = ({ port, token }) => `http://${HOST}:${port}/?token=${token}`;
25
- async function show(info, open, dir) {
26
- console.log(`Open it at ${link(info)}`);
27
- if (open && !(await openBrowser(link(info), dir)))
25
+ // The token swap redirects to `/` with no fragment, so the browser keeps this one: the board opens.
26
+ const link = ({ port, token }, project) => `http://${HOST}:${port}/?token=${token}${project ? `#/projects/${project}` : ''}`;
27
+ async function show(info, open, dir, project) {
28
+ console.log(`Open it at ${link(info, project)}`);
29
+ if (open && !(await openBrowser(link(info, project), dir)))
28
30
  console.log('Could not open a browser; open the link above.');
29
31
  }
32
+ /** The folder this command runs in, as a checkout to connect: a worktree stands for its clone's main checkout. */
33
+ async function here() {
34
+ const checkout = await mainCheckout(process.cwd());
35
+ if (checkout.kind === 'main')
36
+ return { path: checkout.root, worktree: false };
37
+ // A bare clone's worktree has no main checkout: nothing to connect, said as outside a project.
38
+ if (checkout.kind === 'worktree' && checkout.main)
39
+ return { path: checkout.main, worktree: true };
40
+ return undefined;
41
+ }
42
+ /**
43
+ * Connects the folder through the instance and returns the project's id. Outside a Jig project it says
44
+ * nothing: the command then only starts, as it always did.
45
+ */
46
+ async function connectHere(info, place) {
47
+ if (!place)
48
+ return undefined;
49
+ const result = await connectThrough(info, place.path);
50
+ if ('id' in result) {
51
+ const what = place.worktree ? `the main checkout of this worktree, ${place.path}` : place.path;
52
+ console.log(result.created ? `Connected ${result.id}: ${what}.` : `${result.id} is connected: ${what}.`);
53
+ return result.id;
54
+ }
55
+ if (result.code !== 'not-a-repository' && result.code !== 'no-jig')
56
+ console.log(`Could not connect this folder: ${result.message}`);
57
+ return undefined;
58
+ }
30
59
  /** The instance recorded in the config directory, if it still answers with its token. */
31
60
  async function runningInstance(dir) {
32
61
  const info = await readInstance(dir);
@@ -56,13 +85,14 @@ export async function main() {
56
85
  }
57
86
  const { port, open } = command;
58
87
  const dir = configDir(process.env, process.platform, homedir());
88
+ const place = await here();
59
89
  const running = await runningInstance(dir);
60
90
  if (running) {
61
91
  console.log(`Jig UI is already running on port ${running.port}.`);
62
92
  // ponytail: a --port equal to the default is indistinguishable from no flag; no note then.
63
93
  if (port !== running.port && port !== DEFAULT_PORT)
64
94
  console.log(`--port ${port} is ignored.`);
65
- await show(running, open, dir);
95
+ await show(running, open, dir, await connectHere(running, place));
66
96
  return;
67
97
  }
68
98
  const version = readPackageVersion();
@@ -71,7 +101,7 @@ export async function main() {
71
101
  void (async () => {
72
102
  await writeInstance(dir, { port, token, pid: process.pid, version });
73
103
  console.log(`Jig UI ${version} is running at http://${HOST}:${port}/`);
74
- await show({ port, token }, open, dir);
104
+ await show({ port, token }, open, dir, await connectHere({ port, token }, place));
75
105
  console.log('Press Ctrl+C to stop.');
76
106
  })().catch((error) => fail(`Could not start: ${error instanceof Error ? error.message : String(error)}`));
77
107
  });
@@ -83,7 +113,7 @@ export async function main() {
83
113
  if (!winner)
84
114
  fail(`Port ${port} is already in use. Stop the other process or choose another port with --port.`);
85
115
  console.log(`Jig UI is already running on port ${winner.port}.`);
86
- await show(winner, open, dir);
116
+ await show(winner, open, dir, await connectHere(winner, place));
87
117
  process.exit(0);
88
118
  });
89
119
  });
@@ -48,3 +48,24 @@ export async function probeInstance(info, timeoutMs = 1000) {
48
48
  return false;
49
49
  }
50
50
  }
51
+ /** Asks the instance to connect `path`, as the page does: the instance owns the registry, so no second process writes it. */
52
+ export async function connectThrough(info, path, timeoutMs = 15_000) {
53
+ try {
54
+ const res = await fetch(`http://127.0.0.1:${info.port}/api/projects`, {
55
+ method: 'POST',
56
+ headers: { cookie: `${cookieName(info.port)}=${info.token}`, 'content-type': 'application/json' },
57
+ body: JSON.stringify({ path }),
58
+ signal: AbortSignal.timeout(timeoutMs),
59
+ });
60
+ // A failure the instance did not foresee answers in plain text.
61
+ const body = (await res.json().catch(() => ({})));
62
+ if (body.project)
63
+ return { id: body.project.id, created: res.status === 201 };
64
+ if (typeof body.error === 'object')
65
+ return body.error;
66
+ return { code: 'unexpected', message: `Jig UI answered ${res.status}.` };
67
+ }
68
+ catch (error) {
69
+ return { code: 'unreachable', message: error instanceof Error ? error.message : String(error) };
70
+ }
71
+ }
@@ -21,5 +21,23 @@ export async function mainCheckout(path) {
21
21
  const [top, own, common] = stdout.trim().split(/\r?\n/);
22
22
  if (!top || !own || !common)
23
23
  return { kind: 'not-a-repository' };
24
- return (await realpath(own)) === (await realpath(common)) ? { kind: 'main', root: await realpath(top) } : { kind: 'worktree' };
24
+ if ((await realpath(own)) === (await realpath(common)))
25
+ return { kind: 'main', root: await realpath(top) };
26
+ return { kind: 'worktree', main: await mainWorktree(path) };
27
+ }
28
+ /**
29
+ * The main working tree, which `git worktree list` names first — not `dirname(--git-common-dir)`, which
30
+ * misses for a worktree of a submodule. A bare clone's first entry is the repository itself.
31
+ */
32
+ async function mainWorktree(path) {
33
+ try {
34
+ const { stdout } = await run('git', ['worktree', 'list', '--porcelain'], { cwd: path, timeout: 10_000 });
35
+ const [first, second] = stdout.split(/\r?\n/);
36
+ if (!first?.startsWith('worktree ') || second === 'bare')
37
+ return null;
38
+ return await realpath(first.slice('worktree '.length));
39
+ }
40
+ catch {
41
+ return null;
42
+ }
25
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fapost/jig-ui",
3
- "version": "0.1.0-next.16",
3
+ "version": "0.1.0-next.18",
4
4
  "description": "A local web interface for Jig, the agent SDLC framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",