@fapost/jig-ui 0.2.0-next.21 → 0.2.0-next.23

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.
@@ -1,6 +1,9 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ /** What a running instance records so that a second start can find it. */
6
+ const InstanceInfo = z.object({ port: z.int(), token: z.string(), pid: z.int(), version: z.string() });
4
7
  const FILE = 'instance.json';
5
8
  /** The page that sends the browser on to the instance; it carries the token, so it lives here too. */
6
9
  export const OPEN_FILE = 'open.html';
@@ -8,10 +11,7 @@ export const OPEN_FILE = 'open.html';
8
11
  export const cookieName = (port) => `jig-ui-${port}`;
9
12
  export async function readInstance(dir) {
10
13
  try {
11
- const info = JSON.parse(await readFile(join(dir, FILE), 'utf8'));
12
- if (typeof info.port !== 'number' || typeof info.token !== 'string')
13
- return undefined;
14
- return info;
14
+ return InstanceInfo.safeParse(JSON.parse(await readFile(join(dir, FILE), 'utf8'))).data;
15
15
  }
16
16
  catch {
17
17
  return undefined;
@@ -48,6 +48,11 @@ export async function probeInstance(info, timeoutMs = 1000) {
48
48
  return false;
49
49
  }
50
50
  }
51
+ // An instance before 0.2.0 answers a failure it did not foresee in plain text: neither of these.
52
+ const Answer = z.union([
53
+ z.object({ project: z.object({ id: z.string() }) }),
54
+ z.object({ error: z.object({ code: z.string(), message: z.string() }) }),
55
+ ]);
51
56
  /** Asks the instance to connect `path`, as the page does: the instance owns the registry, so no second process writes it. */
52
57
  export async function connectThrough(info, path, timeoutMs = 15_000) {
53
58
  try {
@@ -57,13 +62,10 @@ export async function connectThrough(info, path, timeoutMs = 15_000) {
57
62
  body: JSON.stringify({ path }),
58
63
  signal: AbortSignal.timeout(timeoutMs),
59
64
  });
60
- // An instance before 0.2.0 answers a failure it did not foresee in plain text.
61
- const body = (await res.json().catch(() => ({})));
62
- if (body.project)
65
+ const body = Answer.safeParse(await res.json().catch(() => undefined)).data;
66
+ if (body && 'project' in body)
63
67
  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}.` };
68
+ return body?.error ?? { code: 'unexpected', message: `Jig UI answered ${res.status}.` };
67
69
  }
68
70
  catch (error) {
69
71
  return { code: 'unreachable', message: error instanceof Error ? error.message : String(error) };
@@ -1,5 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
+ import { z } from 'zod';
3
4
  import { writeConfigFile } from './instance.js';
4
5
  const FILE = 'projects.json';
5
6
  /** The registry file exists but cannot be read; it is never overwritten then, so the list is not lost. */
@@ -8,7 +9,8 @@ export class RegistryUnreadable extends Error {
8
9
  super(`${FILE} in Jig UI's config directory cannot be read; fix or remove it.`);
9
10
  }
10
11
  }
11
- const isProject = (p) => typeof p === 'object' && p !== null && typeof p.id === 'string' && typeof p.path === 'string';
12
+ // A row keeps the fields it does not know, so a later version's additions survive this one's writes.
13
+ const Registry = z.object({ projects: z.array(z.looseObject({ id: z.string(), path: z.string() })) });
12
14
  /** The connected projects in the order they were connected; none when there is no registry yet. */
13
15
  export async function readProjects(dir) {
14
16
  let text;
@@ -20,15 +22,17 @@ export async function readProjects(dir) {
20
22
  return [];
21
23
  throw new RegistryUnreadable();
22
24
  }
25
+ let json;
23
26
  try {
24
- const { projects } = JSON.parse(text);
25
- if (Array.isArray(projects) && projects.every(isProject))
26
- return projects;
27
+ json = JSON.parse(text);
27
28
  }
28
29
  catch {
29
- // falls through
30
+ throw new RegistryUnreadable();
30
31
  }
31
- throw new RegistryUnreadable();
32
+ const registry = Registry.safeParse(json);
33
+ if (!registry.success)
34
+ throw new RegistryUnreadable();
35
+ return registry.data.projects;
32
36
  }
33
37
  /** The id for a checkout: its directory name as a slug, with `-2`, `-3`… when that is taken. */
34
38
  export function slugFor(root, taken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fapost/jig-ui",
3
- "version": "0.2.0-next.21",
3
+ "version": "0.2.0-next.23",
4
4
  "description": "A local web interface for Jig, the agent SDLC framework.",
5
5
  "license": "MIT",
6
6
  "type": "module",