ghostrail 0.8.0 → 0.10.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.
Files changed (51) hide show
  1. package/dist/cli/commands.d.ts +32 -1
  2. package/dist/cli/commands.d.ts.map +1 -1
  3. package/dist/cli/commands.js +409 -5
  4. package/dist/cli/commands.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +11 -1
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/help.d.ts.map +1 -1
  9. package/dist/commands/help.js +49 -0
  10. package/dist/commands/help.js.map +1 -1
  11. package/dist/dashboard/index.d.ts +4 -0
  12. package/dist/dashboard/index.d.ts.map +1 -0
  13. package/dist/dashboard/index.js +4 -0
  14. package/dist/dashboard/index.js.map +1 -0
  15. package/dist/dashboard/page.d.ts +10 -0
  16. package/dist/dashboard/page.d.ts.map +1 -0
  17. package/dist/dashboard/page.js +94 -0
  18. package/dist/dashboard/page.js.map +1 -0
  19. package/dist/dashboard/routes.d.ts +48 -0
  20. package/dist/dashboard/routes.d.ts.map +1 -0
  21. package/dist/dashboard/routes.js +102 -0
  22. package/dist/dashboard/routes.js.map +1 -0
  23. package/dist/dashboard/server.d.ts +43 -0
  24. package/dist/dashboard/server.d.ts.map +1 -0
  25. package/dist/dashboard/server.js +110 -0
  26. package/dist/dashboard/server.js.map +1 -0
  27. package/dist/global/index.d.ts +1 -0
  28. package/dist/global/index.d.ts.map +1 -1
  29. package/dist/global/index.js +1 -0
  30. package/dist/global/index.js.map +1 -1
  31. package/dist/global/registry.d.ts +63 -0
  32. package/dist/global/registry.d.ts.map +1 -0
  33. package/dist/global/registry.js +120 -0
  34. package/dist/global/registry.js.map +1 -0
  35. package/dist/global/store.d.ts +10 -0
  36. package/dist/global/store.d.ts.map +1 -1
  37. package/dist/global/store.js +25 -1
  38. package/dist/global/store.js.map +1 -1
  39. package/dist/service/index.d.ts +3 -0
  40. package/dist/service/index.d.ts.map +1 -0
  41. package/dist/service/index.js +3 -0
  42. package/dist/service/index.js.map +1 -0
  43. package/dist/service/manager.d.ts +71 -0
  44. package/dist/service/manager.d.ts.map +1 -0
  45. package/dist/service/manager.js +125 -0
  46. package/dist/service/manager.js.map +1 -0
  47. package/dist/service/unit.d.ts +71 -0
  48. package/dist/service/unit.d.ts.map +1 -0
  49. package/dist/service/unit.js +138 -0
  50. package/dist/service/unit.js.map +1 -0
  51. package/package.json +1 -1
@@ -0,0 +1,102 @@
1
+ /** Map a request pathname to a route. Total; never throws. */
2
+ export function parseDashboardRoute(pathname) {
3
+ const parts = pathname.split("/").filter((p) => p.length > 0);
4
+ // Nothing but slashes is the root. `//` reaching a server is unusual but a
5
+ // proxy or a hand-built link produces it, and serving the page beats a 404
6
+ // for something that means the root by every reading.
7
+ if (parts.length === 0)
8
+ return { kind: "page" };
9
+ if (parts[0] !== "api" || parts.length !== 2)
10
+ return { kind: "notfound" };
11
+ if (parts[1] === "ghostrails")
12
+ return { kind: "ghostrails" };
13
+ if (parts[1] === "watch")
14
+ return { kind: "watch" };
15
+ return { kind: "notfound" };
16
+ }
17
+ /**
18
+ * Every value for a header, case-insensitively.
19
+ *
20
+ * All of them, not the first. Node hands back an array for a repeated header,
21
+ * and a guard that inspects only `value[0]` can be walked past by sending the
22
+ * innocent spelling first. For a check whose whole job is refusing a request,
23
+ * "any value looks like an attack" is the only safe reading.
24
+ *
25
+ * Case-insensitive on the name because Node lowercases them but a hand-built
26
+ * object does not have to.
27
+ */
28
+ function headerValues(headers, name) {
29
+ const wanted = name.toLowerCase();
30
+ const values = [];
31
+ for (const [key, value] of Object.entries(headers)) {
32
+ if (key.toLowerCase() !== wanted || value === undefined)
33
+ continue;
34
+ if (Array.isArray(value))
35
+ values.push(...value);
36
+ else
37
+ values.push(String(value));
38
+ }
39
+ return values;
40
+ }
41
+ /** The three content types a form can send without a CORS preflight. */
42
+ const FORM_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
43
+ /**
44
+ * True when a request looks cross-origin, which the write route refuses.
45
+ *
46
+ * This is not the token decision (TJ-1554 owns that). It is hygiene for a
47
+ * mutating route: a plain HTML form on any website can POST to a loopback port
48
+ * cross-origin, and by inherited board behaviour there is no token in the way
49
+ * unless the bind is non-loopback. Without this, any page the operator visits
50
+ * could flip their watch flags.
51
+ *
52
+ * A request with neither signal, such as `curl` sending JSON, is allowed.
53
+ * Locking that out is a token's job: anything able to set arbitrary headers can
54
+ * also read a token, so refusing it would break scripting for no gain.
55
+ */
56
+ export function isCrossOriginWrite(headers) {
57
+ // Browsers send this unconditionally and script cannot forge it. Any value
58
+ // that is not same-origin rejects, so a repeated header cannot smuggle one
59
+ // past by leading with the innocent spelling.
60
+ const sites = headerValues(headers, "sec-fetch-site");
61
+ if (sites.some((site) => site.trim().toLowerCase() !== "same-origin"))
62
+ return true;
63
+ // Requiring a non-form content type forces a preflight a form cannot satisfy.
64
+ return headerValues(headers, "content-type").some((value) => {
65
+ const media = value.split(";")[0]?.trim().toLowerCase() ?? "";
66
+ return FORM_TYPES.includes(media);
67
+ });
68
+ }
69
+ /** True if `value` is a plain object (not null, not an array). */
70
+ function isRecord(value) {
71
+ return typeof value === "object" && value !== null && !Array.isArray(value);
72
+ }
73
+ /**
74
+ * Parse the write body: `{ path, watch }`. Undefined for anything else, which
75
+ * the route answers with 400.
76
+ *
77
+ * An empty `path` is rejected rather than passed through: it can never match a
78
+ * registered ghostrail, so accepting it would only produce a confusing 404.
79
+ */
80
+ /**
81
+ * Unknown keys alongside the two it needs are ignored, and the entry still
82
+ * parses. Same forward compatibility the registry uses: a later dashboard that
83
+ * sends an extra field must not make an older server reject the write outright.
84
+ */
85
+ export function parseWatchUpdate(body) {
86
+ let value;
87
+ try {
88
+ value = JSON.parse(body);
89
+ }
90
+ catch {
91
+ return undefined;
92
+ }
93
+ if (!isRecord(value))
94
+ return undefined;
95
+ const { path, watch } = value;
96
+ if (typeof path !== "string" || path.length === 0)
97
+ return undefined;
98
+ if (typeof watch !== "boolean")
99
+ return undefined;
100
+ return { path, watch };
101
+ }
102
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/dashboard/routes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { type Server } from "node:http";
2
+ import type { Ghostrail } from "../global/registry.js";
3
+ /** How a ghostrail stands on disk, mirroring what `status` reports. */
4
+ export type GhostrailHealth = "ok" | "no config" | "missing";
5
+ /** One row of the dashboard: registry entry plus everything observed about it. */
6
+ export interface GhostrailView extends Ghostrail {
7
+ health: GhostrailHealth;
8
+ /** Live unit state, e.g. `active`. `unknown` when no manager could be asked. */
9
+ state: string;
10
+ /** Most recent runs from this ghostrail's own `.factory`, newest first. */
11
+ runs: {
12
+ runId: string;
13
+ itemId?: string;
14
+ state?: string;
15
+ title?: string;
16
+ }[];
17
+ }
18
+ /** Everything the server needs, injected so it can be driven without a machine. */
19
+ export interface DashboardDeps {
20
+ /** The registry, or undefined when it could not be parsed. */
21
+ readRegistry(): Promise<Ghostrail[] | undefined>;
22
+ writeRegistry(entries: readonly Ghostrail[]): Promise<void>;
23
+ health(path: string): Promise<GhostrailHealth>;
24
+ /** Live unit state per ghostrail path. */
25
+ states(entries: readonly Ghostrail[]): Promise<Map<string, string>>;
26
+ }
27
+ export interface DashboardOptions {
28
+ bind?: string;
29
+ /** When set, every request must present it. Inherited from the board's behaviour. */
30
+ token?: string;
31
+ }
32
+ /** Assemble the view for every registered ghostrail. */
33
+ export declare function collectViews(deps: DashboardDeps, entries: readonly Ghostrail[]): Promise<GhostrailView[]>;
34
+ /**
35
+ * The dashboard's HTTP server: one page, one listing, and one write route that
36
+ * sets a ghostrail's `watch` flag and does nothing else.
37
+ *
38
+ * It never starts or stops anything. `start` and the service units act on the
39
+ * flag, which keeps process control off an HTTP surface: the worst a
40
+ * compromised dashboard can do is change which ghostrails come up next time.
41
+ */
42
+ export declare function createDashboardServer(deps: DashboardDeps, options?: DashboardOptions): Server;
43
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAqC,MAAM,WAAW,CAAC;AAE3E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAKvD,uEAAuE;AACvE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D,kFAAkF;AAClF,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,MAAM,EAAE,eAAe,CAAC;IACxB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5E;AAED,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC,CAAC;IACjD,aAAa,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC/C,0CAA0C;IAC1C,MAAM,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrE;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD,wDAAwD;AACxD,wBAAsB,YAAY,CAChC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,SAAS,SAAS,EAAE,GAC5B,OAAO,CAAC,aAAa,EAAE,CAAC,CAwB1B;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAsDjG"}
@@ -0,0 +1,110 @@
1
+ import { createServer } from "node:http";
2
+ import { join } from "node:path";
3
+ import { FileStore } from "../store/file-store.js";
4
+ import { DASHBOARD_PAGE_HTML } from "./page.js";
5
+ import { isCrossOriginWrite, parseDashboardRoute, parseWatchUpdate } from "./routes.js";
6
+ /** The store dir inside a ghostrail. */
7
+ const STORE_DIR = ".factory";
8
+ /** How many runs to show per ghostrail. Enough to see recent activity, not a log. */
9
+ const RUNS_PER_GHOSTRAIL = 5;
10
+ function sendJson(res, status, data) {
11
+ res.writeHead(status, { "content-type": "application/json" });
12
+ res.end(JSON.stringify(data));
13
+ }
14
+ /** Read a request body, bounded so a stray large POST cannot exhaust memory. */
15
+ async function readBody(req, limit = 64 * 1024) {
16
+ const chunks = [];
17
+ let size = 0;
18
+ for await (const chunk of req) {
19
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
20
+ size += buf.length;
21
+ if (size > limit)
22
+ break;
23
+ chunks.push(buf);
24
+ }
25
+ return Buffer.concat(chunks).toString("utf8");
26
+ }
27
+ /** Assemble the view for every registered ghostrail. */
28
+ export async function collectViews(deps, entries) {
29
+ const states = await deps.states(entries);
30
+ const views = [];
31
+ for (const entry of entries) {
32
+ const health = await deps.health(entry.path);
33
+ // A ghostrail with no .factory yet is still a ghostrail; it just has
34
+ // nothing to report. Never let a missing store turn a row into an error.
35
+ let runs = [];
36
+ if (health === "ok") {
37
+ try {
38
+ const rows = await new FileStore(join(entry.path, STORE_DIR)).listRuns({});
39
+ runs = rows.slice(0, RUNS_PER_GHOSTRAIL).map((row) => ({
40
+ runId: row.runId,
41
+ ...(row.itemId === undefined ? {} : { itemId: row.itemId }),
42
+ ...(row.state === undefined ? {} : { state: row.state }),
43
+ ...(row.title === undefined ? {} : { title: row.title }),
44
+ }));
45
+ }
46
+ catch {
47
+ runs = [];
48
+ }
49
+ }
50
+ views.push({ ...entry, health, state: states.get(entry.path) ?? "unknown", runs });
51
+ }
52
+ return views;
53
+ }
54
+ /**
55
+ * The dashboard's HTTP server: one page, one listing, and one write route that
56
+ * sets a ghostrail's `watch` flag and does nothing else.
57
+ *
58
+ * It never starts or stops anything. `start` and the service units act on the
59
+ * flag, which keeps process control off an HTTP surface: the worst a
60
+ * compromised dashboard can do is change which ghostrails come up next time.
61
+ */
62
+ export function createDashboardServer(deps, options = {}) {
63
+ return createServer((req, res) => {
64
+ void (async () => {
65
+ const url = new URL(req.url ?? "/", "http://localhost");
66
+ if (options.token !== undefined) {
67
+ const presented = req.headers["x-ghostrail-token"] ?? url.searchParams.get("token") ?? "";
68
+ if (presented !== options.token)
69
+ return sendJson(res, 401, { error: "unauthorized" });
70
+ }
71
+ const route = parseDashboardRoute(url.pathname);
72
+ if (route.kind === "page") {
73
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
74
+ return res.end(DASHBOARD_PAGE_HTML);
75
+ }
76
+ const entries = await deps.readRegistry();
77
+ if (entries === undefined) {
78
+ return sendJson(res, 500, { error: "the ghostrails registry could not be parsed" });
79
+ }
80
+ if (route.kind === "ghostrails") {
81
+ return sendJson(res, 200, { ghostrails: await collectViews(deps, entries) });
82
+ }
83
+ if (route.kind === "watch") {
84
+ if (req.method !== "POST")
85
+ return sendJson(res, 405, { error: "use POST" });
86
+ // Hygiene for a mutating route, not the token decision (TJ-1554). A
87
+ // form on any site can POST here cross-origin, and by inherited board
88
+ // behaviour there is no token in the way on loopback.
89
+ if (isCrossOriginWrite(req.headers)) {
90
+ return sendJson(res, 403, { error: "cross-origin writes are refused" });
91
+ }
92
+ const update = parseWatchUpdate(await readBody(req));
93
+ if (update === undefined) {
94
+ return sendJson(res, 400, { error: "expected {path: string, watch: boolean}" });
95
+ }
96
+ // Must already be registered. Registering an arbitrary filesystem path
97
+ // over HTTP is a far larger surface than this route wants to be, and
98
+ // registration is `init`'s job.
99
+ if (!entries.some((e) => e.path === update.path)) {
100
+ return sendJson(res, 404, { error: "no registered ghostrail at that path" });
101
+ }
102
+ const next = entries.map((e) => e.path === update.path ? { ...e, watch: update.watch } : e);
103
+ await deps.writeRegistry(next);
104
+ return sendJson(res, 200, { ghostrails: await collectViews(deps, next) });
105
+ }
106
+ return sendJson(res, 404, { error: "not found" });
107
+ })().catch(() => sendJson(res, 500, { error: "internal error" }));
108
+ });
109
+ }
110
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/dashboard/server.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ export { type Ghostrail, REGISTRY_FILE, findByPath, parseRegistry, renderRegistry, upsert, } from "./registry.js";
1
2
  export { artifactFileName, type GlobalEnv, GlobalStore, resolveGlobalDir } from "./store.js";
2
3
  export { STAGED_ARTIFACTS_DIR, stageArtifacts } from "./stage.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/global/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/global/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EACd,aAAa,EACb,UAAU,EACV,aAAa,EACb,cAAc,EACd,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,KAAK,SAAS,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
@@ -1,3 +1,4 @@
1
+ export { REGISTRY_FILE, findByPath, parseRegistry, renderRegistry, upsert, } from "./registry.js";
1
2
  export { artifactFileName, GlobalStore, resolveGlobalDir } from "./store.js";
2
3
  export { STAGED_ARTIFACTS_DIR, stageArtifacts } from "./stage.js";
3
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/global/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAkB,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/global/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,EACb,UAAU,EACV,aAAa,EACb,cAAc,EACd,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAkB,WAAW,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The registry of ghostrails: which repos this account has set up, so
3
+ * `start` has something to offer and the board has something to list. Lives
4
+ * beside `settings.toml` in the global dir.
5
+ *
6
+ * Everything here is pure. Reading and writing the file is the store's job, so
7
+ * a malformed registry can be tested without a filesystem.
8
+ */
9
+ /** One repo ghostrail knows about. */
10
+ export interface Ghostrail {
11
+ /** Display name, from `[factory].name` or the directory. Never an identity. */
12
+ name: string;
13
+ /** Absolute path to the repo. This is the identity. */
14
+ path: string;
15
+ /** ISO date the entry was added, for the operator. Nothing reads it. */
16
+ added: string;
17
+ /**
18
+ * Desired state: whether `start` should bring this one up. Not live state.
19
+ * One flag, so the CLI, the board, and the service units cannot disagree.
20
+ */
21
+ watch: boolean;
22
+ }
23
+ /** The file name in the global dir. */
24
+ export declare const REGISTRY_FILE = "ghostrails.toml";
25
+ /**
26
+ * Parse the registry, or undefined when it cannot be trusted.
27
+ *
28
+ * All or nothing, exactly as `readManifest` treats a malformed manifest: a
29
+ * half-trusted registry is worse than none, because silently dropping one bad
30
+ * entry would quietly stop watching a ghostrail with no signal that anything
31
+ * was wrong.
32
+ *
33
+ * A file with no `[[ghostrail]]` array is an empty registry, not a broken one:
34
+ * nothing registered yet is a real state, and so is an explicitly empty
35
+ * `ghostrail = []`. Unknown keys on an otherwise valid entry are ignored rather
36
+ * than fatal, so a registry written by a later ghostrail still reads here.
37
+ * Order is preserved exactly, and the array is fresh on every call.
38
+ *
39
+ * Two entries at the same path are malformed for the same reason: a parsed
40
+ * registry never contains duplicate paths, which is what lets `upsert` and
41
+ * `findByPath` stay as simple as they are.
42
+ */
43
+ export declare function parseRegistry(toml: string): Ghostrail[] | undefined;
44
+ /**
45
+ * Render entries back to TOML, in the order given. Round-trips through
46
+ * {@link parseRegistry}, which is what keeps a rewrite from mangling a file the
47
+ * operator has edited.
48
+ *
49
+ * The layout is part of the contract: keys padded so the `=` signs line up, one
50
+ * blank line between blocks. Both are for the human who opens this file, which
51
+ * the header invites them to do.
52
+ */
53
+ export declare function renderRegistry(entries: readonly Ghostrail[]): string;
54
+ /** The entry at `path`, or undefined. Exact string equality; callers resolve first. */
55
+ export declare function findByPath(entries: readonly Ghostrail[], path: string): Ghostrail | undefined;
56
+ /**
57
+ * Add `entry`, or replace the one at the same path.
58
+ *
59
+ * A replacement keeps its position so a diff of the file shows a changed block
60
+ * rather than a move; a new entry appends. Neither argument is mutated.
61
+ */
62
+ export declare function upsert(entries: readonly Ghostrail[], entry: Ghostrail): Ghostrail[];
63
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/global/registry.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AAEH,sCAAsC;AACtC,MAAM,WAAW,SAAS;IACxB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,EAAE,OAAO,CAAC;CAChB;AAED,uCAAuC;AACvC,eAAO,MAAM,aAAa,oBAAoB,CAAC;AAyB/C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,SAAS,CA2BnE;AAOD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,MAAM,CAapE;AAED,uFAAuF;AACvF,wBAAgB,UAAU,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAE7F;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,GAAG,SAAS,EAAE,CAMnF"}
@@ -0,0 +1,120 @@
1
+ import { parse as parseToml } from "smol-toml";
2
+ /** The file name in the global dir. */
3
+ export const REGISTRY_FILE = "ghostrails.toml";
4
+ const HEADER = `# The ghostrails this account knows about: the repos ghostrail is set up for.
5
+ #
6
+ # Written by \`ghostrail init\` and topped up by \`ghostrail init --update\`.
7
+ # \`watch\` is desired state, what \`ghostrail start\` should bring up, not a
8
+ # statement about what is running right now.
9
+ #
10
+ # Safe to hand-edit. A file that does not parse is treated as no registry at
11
+ # all rather than half-trusted, so a typo here is loud, not silent.`;
12
+ /** True if `value` is a plain object (not null, not an array). */
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ /** One entry, or undefined if any required field is missing or wrong-typed. */
17
+ function parseEntry(raw) {
18
+ if (!isRecord(raw))
19
+ return undefined;
20
+ const { name, path, added, watch } = raw;
21
+ if (typeof name !== "string" || typeof path !== "string")
22
+ return undefined;
23
+ if (typeof added !== "string" || typeof watch !== "boolean")
24
+ return undefined;
25
+ return { name, path, added, watch };
26
+ }
27
+ /**
28
+ * Parse the registry, or undefined when it cannot be trusted.
29
+ *
30
+ * All or nothing, exactly as `readManifest` treats a malformed manifest: a
31
+ * half-trusted registry is worse than none, because silently dropping one bad
32
+ * entry would quietly stop watching a ghostrail with no signal that anything
33
+ * was wrong.
34
+ *
35
+ * A file with no `[[ghostrail]]` array is an empty registry, not a broken one:
36
+ * nothing registered yet is a real state, and so is an explicitly empty
37
+ * `ghostrail = []`. Unknown keys on an otherwise valid entry are ignored rather
38
+ * than fatal, so a registry written by a later ghostrail still reads here.
39
+ * Order is preserved exactly, and the array is fresh on every call.
40
+ *
41
+ * Two entries at the same path are malformed for the same reason: a parsed
42
+ * registry never contains duplicate paths, which is what lets `upsert` and
43
+ * `findByPath` stay as simple as they are.
44
+ */
45
+ export function parseRegistry(toml) {
46
+ let root;
47
+ try {
48
+ root = parseToml(toml);
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ if (!isRecord(root))
54
+ return undefined;
55
+ const raw = root.ghostrail;
56
+ if (raw === undefined)
57
+ return [];
58
+ if (!Array.isArray(raw))
59
+ return undefined;
60
+ const entries = [];
61
+ const paths = new Set();
62
+ for (const item of raw) {
63
+ const entry = parseEntry(item);
64
+ if (entry === undefined)
65
+ return undefined;
66
+ // `path` is the identity, so two blocks claiming it is a contradiction
67
+ // rather than a preference to resolve. Copying a block and forgetting to
68
+ // change the path is a plausible hand-edit, and honouring either one of
69
+ // them would silently ignore an edit the operator clearly meant.
70
+ if (paths.has(entry.path))
71
+ return undefined;
72
+ paths.add(entry.path);
73
+ entries.push(entry);
74
+ }
75
+ return entries;
76
+ }
77
+ /** TOML-quote a string. The values here are paths and names, not arbitrary data. */
78
+ function quote(value) {
79
+ return JSON.stringify(value);
80
+ }
81
+ /**
82
+ * Render entries back to TOML, in the order given. Round-trips through
83
+ * {@link parseRegistry}, which is what keeps a rewrite from mangling a file the
84
+ * operator has edited.
85
+ *
86
+ * The layout is part of the contract: keys padded so the `=` signs line up, one
87
+ * blank line between blocks. Both are for the human who opens this file, which
88
+ * the header invites them to do.
89
+ */
90
+ export function renderRegistry(entries) {
91
+ const blocks = entries.map((entry) => [
92
+ "[[ghostrail]]",
93
+ `name = ${quote(entry.name)}`,
94
+ `path = ${quote(entry.path)}`,
95
+ `added = ${quote(entry.added)}`,
96
+ `watch = ${entry.watch}`,
97
+ ].join("\n"));
98
+ // A blank line between blocks. This file is advertised as hand-editable, and
99
+ // run together the blocks are hard to read and give noisier diffs.
100
+ return `${[HEADER, ...blocks].join("\n\n")}\n`;
101
+ }
102
+ /** The entry at `path`, or undefined. Exact string equality; callers resolve first. */
103
+ export function findByPath(entries, path) {
104
+ return entries.find((entry) => entry.path === path);
105
+ }
106
+ /**
107
+ * Add `entry`, or replace the one at the same path.
108
+ *
109
+ * A replacement keeps its position so a diff of the file shows a changed block
110
+ * rather than a move; a new entry appends. Neither argument is mutated.
111
+ */
112
+ export function upsert(entries, entry) {
113
+ const index = entries.findIndex((existing) => existing.path === entry.path);
114
+ if (index === -1)
115
+ return [...entries, entry];
116
+ const next = [...entries];
117
+ next[index] = entry;
118
+ return next;
119
+ }
120
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/global/registry.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { type Ghostrail } from "./registry.js";
1
2
  export interface GlobalEnv {
2
3
  GHOSTRAIL_HOME?: string;
3
4
  XDG_CONFIG_HOME?: string;
@@ -24,6 +25,15 @@ export declare class GlobalStore {
24
25
  readSettings(): Promise<string | undefined>;
25
26
  /** Artifact base names present in the store, sorted. */
26
27
  listArtifacts(): Promise<string[]>;
28
+ registryPath(): string;
29
+ /**
30
+ * The registered ghostrails. `undefined` means the file exists but could not
31
+ * be trusted, which is deliberately different from `[]` (nothing registered
32
+ * yet): a caller must be able to tell "broken" from "empty" so it can say so.
33
+ */
34
+ readRegistry(): Promise<Ghostrail[] | undefined>;
35
+ /** Write the registry, creating the global dir if this is the first entry. */
36
+ writeRegistry(entries: readonly Ghostrail[]): Promise<void>;
27
37
  /** An artifact's contents by name (with or without `.md`), or undefined. */
28
38
  readArtifact(name: string): Promise<string | undefined>;
29
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOxE;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,EAAE,MAAM;IAIvB,YAAY,IAAI,MAAM;IAItB,mEAAmE;IAC7D,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQjD,wDAAwD;IAClD,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQxC,4EAA4E;IACtE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAU9D"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAgD,MAAM,eAAe,CAAC;AAE7F,MAAM,WAAW,SAAS;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAOxE;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED;;;;GAIG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,GAAG,EAAE,MAAM;IAIvB,YAAY,IAAI,MAAM;IAItB,mEAAmE;IAC7D,YAAY,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAQjD,wDAAwD;IAClD,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAQxC,YAAY,IAAI,MAAM;IAItB;;;;OAIG;IACG,YAAY,IAAI,OAAO,CAAC,SAAS,EAAE,GAAG,SAAS,CAAC;IAUtD,8EAA8E;IACxE,aAAa,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,4EAA4E;IACtE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;CAU9D"}
@@ -1,5 +1,6 @@
1
- import { readFile, readdir } from "node:fs/promises";
1
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { REGISTRY_FILE, parseRegistry, renderRegistry } from "./registry.js";
3
4
  /**
4
5
  * Resolve the global ghostrail config directory that holds account-level
5
6
  * settings and shared artifacts across all factory instances:
@@ -49,6 +50,29 @@ export class GlobalStore {
49
50
  return [];
50
51
  }
51
52
  }
53
+ registryPath() {
54
+ return join(this.dir, REGISTRY_FILE);
55
+ }
56
+ /**
57
+ * The registered ghostrails. `undefined` means the file exists but could not
58
+ * be trusted, which is deliberately different from `[]` (nothing registered
59
+ * yet): a caller must be able to tell "broken" from "empty" so it can say so.
60
+ */
61
+ async readRegistry() {
62
+ let raw;
63
+ try {
64
+ raw = await readFile(this.registryPath(), "utf8");
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ return parseRegistry(raw);
70
+ }
71
+ /** Write the registry, creating the global dir if this is the first entry. */
72
+ async writeRegistry(entries) {
73
+ await mkdir(this.dir, { recursive: true });
74
+ await writeFile(this.registryPath(), renderRegistry(entries), "utf8");
75
+ }
52
76
  /** An artifact's contents by name (with or without `.md`), or undefined. */
53
77
  async readArtifact(name) {
54
78
  for (const candidate of new Set([name, artifactFileName(name)])) {
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAOjC;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAc,EAAE,OAAe;IAC9D,IAAI,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,GAAG,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,cAAc,CAAC;IACjG,MAAM,IAAI,GACR,GAAG,CAAC,eAAe,KAAK,SAAS,IAAI,GAAG,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC;QACjE,CAAC,CAAC,GAAG,CAAC,eAAe;QACrB,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IAC/B,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACjC,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,WAAW;IACL,GAAG,CAAS;IAE7B,YAAY,GAAW;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IACjB,CAAC;IAED,YAAY;QACV,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC;IACzC,CAAC;IAED,mEAAmE;IACnE,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,MAAM,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,wDAAwD;IACxD,KAAK,CAAC,aAAa;QACjB,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,KAAK,MAAM,SAAS,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAChE,IAAI,CAAC;gBACH,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;YACxE,CAAC;YAAC,MAAM,CAAC;gBACP,yBAAyB;YAC3B,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/global/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { type InstallOutcome, type ManagerKind, type ServiceManager, detectManager, disableAndStop, enableAndStart, installUnit, reload, removeUnit, systemdManager, systemdUnitDir, unitStates, } from "./manager.js";
2
+ export { type UnitSpec, DASHBOARD_UNIT, renderDashboardUnit, parseUnitStates, renderSystemdUnit, stopTimeoutSeconds, unitName, } from "./unit.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/service/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,aAAa,EACb,cAAc,EACd,cAAc,EACd,WAAW,EACX,MAAM,EACN,UAAU,EACV,cAAc,EACd,cAAc,EACd,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,QAAQ,EACb,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,GACT,MAAM,WAAW,CAAC"}
@@ -0,0 +1,3 @@
1
+ export { detectManager, disableAndStop, enableAndStart, installUnit, reload, removeUnit, systemdManager, systemdUnitDir, unitStates, } from "./manager.js";
2
+ export { DASHBOARD_UNIT, renderDashboardUnit, parseUnitStates, renderSystemdUnit, stopTimeoutSeconds, unitName, } from "./unit.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/service/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,aAAa,EACb,cAAc,EACd,cAAc,EACd,WAAW,EACX,MAAM,EACN,UAAU,EACV,cAAc,EACd,cAAc,EACd,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,OAAO,EAEL,cAAc,EACd,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,QAAQ,GACT,MAAM,WAAW,CAAC"}