@vincentt-xr/harness 0.3.0 → 1.0.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/dist/client/HarnessProvider.d.ts +5 -0
- package/dist/client/HarnessProvider.js +11 -0
- package/dist/client/annotate.d.ts +34 -0
- package/dist/client/annotate.js +104 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +1 -0
- package/dist/shared/events.d.ts +50 -0
- package/package.json +8 -33
- package/README.md +0 -87
- package/dist/mcp/backend.d.ts +0 -52
- package/dist/mcp/backend.js +0 -146
- package/dist/mcp/cli.d.ts +0 -2
- package/dist/mcp/cli.js +0 -10
- package/dist/mcp/diagnostics.d.ts +0 -13
- package/dist/mcp/diagnostics.js +0 -61
- package/dist/mcp/server.d.ts +0 -16
- package/dist/mcp/server.js +0 -221
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -37
- package/dist/preview/index.d.ts +0 -3
- package/dist/preview/index.js +0 -6
- package/dist/preview/net.d.ts +0 -6
- package/dist/preview/net.js +0 -56
- package/dist/preview/proxy.d.ts +0 -4
- package/dist/preview/proxy.js +0 -49
- package/dist/preview/runner.d.ts +0 -43
- package/dist/preview/runner.js +0 -110
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
- package/dist/relay/cli.d.ts +0 -2
- package/dist/relay/cli.js +0 -7
- package/dist/relay/server.d.ts +0 -12
- package/dist/relay/server.js +0 -85
- package/dist/relay/store.d.ts +0 -13
- package/dist/relay/store.js +0 -68
- package/dist/scaffold/index.d.ts +0 -26
- package/dist/scaffold/index.js +0 -85
- package/dist/shared/config.d.ts +0 -33
- package/dist/shared/config.js +0 -76
package/dist/preview/tunnel.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Per-session dev tunnel: resolve the project binding + machine config and mint a
|
|
2
|
-
// named tunnel via the backend (which holds the Cloudflare account creds). Stays
|
|
3
|
-
// free of any child-process / cloudflared coupling — the caller runs cloudflared
|
|
4
|
-
// with the returned run token (see runner.ts) and calls reap() on exit.
|
|
5
|
-
import { loadProjectBinding, resolveConfig } from "../shared/config.js";
|
|
6
|
-
import { mintTunnel, reapTunnel } from "../mcp/backend.js";
|
|
7
|
-
/**
|
|
8
|
-
* Mint a per-session dev tunnel for the project bound to `projectCwd`, routing
|
|
9
|
-
* <slug>-<token>.<apex> to the local `localPort`. Returns the URL to open and a
|
|
10
|
-
* `reap()` for teardown. Throws with an actionable message when the directory is
|
|
11
|
-
* unbound (run project_create) or no backend/PAT is configured.
|
|
12
|
-
*/
|
|
13
|
-
export async function startSessionTunnel(projectCwd, localPort) {
|
|
14
|
-
const binding = await loadProjectBinding(projectCwd);
|
|
15
|
-
if (!binding) {
|
|
16
|
-
throw new Error("No project bound to this directory — run project_create first.");
|
|
17
|
-
}
|
|
18
|
-
const cfg = await resolveConfig(projectCwd);
|
|
19
|
-
const minted = await mintTunnel(cfg, binding.projectId, localPort);
|
|
20
|
-
return {
|
|
21
|
-
...minted,
|
|
22
|
-
url: `https://${minted.hostname}`,
|
|
23
|
-
reap: () => reapTunnel(cfg, binding.projectId, {
|
|
24
|
-
tunnelId: minted.tunnelId,
|
|
25
|
-
dnsRecordId: minted.dnsRecordId,
|
|
26
|
-
}),
|
|
27
|
-
};
|
|
28
|
-
}
|
package/dist/relay/cli.d.ts
DELETED
package/dist/relay/cli.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// `harness-relay` — start the diagnostics relay beside Vite. The preview loop
|
|
3
|
-
// spawns this (or you run it directly). Prints a line the tunnel step can read.
|
|
4
|
-
import { startRelay } from "./server.js";
|
|
5
|
-
const portArg = process.argv.indexOf("--port");
|
|
6
|
-
const port = portArg !== -1 ? Number(process.argv[portArg + 1]) : 7331;
|
|
7
|
-
startRelay({ port, onLog: (m) => console.log(`[harness-relay] ${m}`) });
|
package/dist/relay/server.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { EventStore } from "./store.js";
|
|
2
|
-
export interface RelayOptions {
|
|
3
|
-
port?: number;
|
|
4
|
-
/** WS path the client connects to (default matches HarnessProvider). */
|
|
5
|
-
path?: string;
|
|
6
|
-
capacity?: number;
|
|
7
|
-
onLog?: (msg: string) => void;
|
|
8
|
-
}
|
|
9
|
-
export declare function startRelay(opts?: RelayOptions): {
|
|
10
|
-
store: EventStore;
|
|
11
|
-
close: () => Promise<void>;
|
|
12
|
-
};
|
package/dist/relay/server.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// The relay: a WebSocket + HTTP server that sits beside Vite. The phone's
|
|
2
|
-
// harness client pushes event batches over WS; the MCP server pulls via a
|
|
3
|
-
// localhost HTTP endpoint. Deliberately tiny — all the buffering/querying logic
|
|
4
|
-
// is in store.ts. One relay process per preview run; the cloudflared tunnel
|
|
5
|
-
// carries the WS path to the phone.
|
|
6
|
-
import { createServer } from "node:http";
|
|
7
|
-
import { WebSocketServer } from "ws";
|
|
8
|
-
import { EventStore } from "./store.js";
|
|
9
|
-
export function startRelay(opts = {}) {
|
|
10
|
-
const port = opts.port ?? 7331;
|
|
11
|
-
const path = opts.path ?? "/__harness";
|
|
12
|
-
const store = new EventStore(opts.capacity);
|
|
13
|
-
const log = opts.onLog ?? (() => undefined);
|
|
14
|
-
const http = createServer((req, res) => handleHttp(req, res, store, log));
|
|
15
|
-
const wss = new WebSocketServer({ server: http, path });
|
|
16
|
-
wss.on("connection", (ws) => {
|
|
17
|
-
log("client connected");
|
|
18
|
-
ws.on("message", (data) => {
|
|
19
|
-
try {
|
|
20
|
-
const msg = JSON.parse(String(data));
|
|
21
|
-
if (msg.type === "events" && Array.isArray(msg.events)) {
|
|
22
|
-
store.ingest(msg.sessionId, msg.events);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
// ignore malformed frames — a bad client must not crash the relay
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
ws.on("close", () => log("client disconnected"));
|
|
30
|
-
});
|
|
31
|
-
http.listen(port, () => log(`relay listening on :${port} (ws ${path}, http /query)`));
|
|
32
|
-
return {
|
|
33
|
-
store,
|
|
34
|
-
close: () => new Promise((resolve) => {
|
|
35
|
-
wss.close();
|
|
36
|
-
http.close(() => resolve());
|
|
37
|
-
}),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
// The MCP server (running on the same laptop) GETs /query with the RelayQuery
|
|
41
|
-
// as JSON in the body or querystring. Localhost-only by deployment; no auth in
|
|
42
|
-
// the local dev loop (the platform phase adds it).
|
|
43
|
-
function handleHttp(req, res, store, log) {
|
|
44
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
45
|
-
if (url.pathname !== "/query") {
|
|
46
|
-
res.writeHead(404).end();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
readQuery(req, url)
|
|
50
|
-
.then((q) => {
|
|
51
|
-
const result = store.query(q);
|
|
52
|
-
res
|
|
53
|
-
.writeHead(200, { "content-type": "application/json" })
|
|
54
|
-
.end(JSON.stringify(result));
|
|
55
|
-
})
|
|
56
|
-
.catch((err) => {
|
|
57
|
-
log(`query error: ${err}`);
|
|
58
|
-
res.writeHead(400).end();
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
async function readQuery(req, url) {
|
|
62
|
-
if (req.method === "POST") {
|
|
63
|
-
const body = await readBody(req);
|
|
64
|
-
return body ? JSON.parse(body) : {};
|
|
65
|
-
}
|
|
66
|
-
const q = {};
|
|
67
|
-
const p = url.searchParams;
|
|
68
|
-
if (p.get("sessionId"))
|
|
69
|
-
q.sessionId = p.get("sessionId");
|
|
70
|
-
if (p.get("kind"))
|
|
71
|
-
q.kind = p.get("kind");
|
|
72
|
-
if (p.get("since"))
|
|
73
|
-
q.since = Number(p.get("since"));
|
|
74
|
-
if (p.get("limit"))
|
|
75
|
-
q.limit = Number(p.get("limit"));
|
|
76
|
-
return q;
|
|
77
|
-
}
|
|
78
|
-
function readBody(req) {
|
|
79
|
-
return new Promise((resolve, reject) => {
|
|
80
|
-
let data = "";
|
|
81
|
-
req.on("data", (c) => (data += c));
|
|
82
|
-
req.on("end", () => resolve(data));
|
|
83
|
-
req.on("error", reject);
|
|
84
|
-
});
|
|
85
|
-
}
|
package/dist/relay/store.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { DiagEvent, RelayQuery, RelayResult } from "../shared/events.js";
|
|
2
|
-
export declare class EventStore {
|
|
3
|
-
private readonly capacity;
|
|
4
|
-
private readonly buf;
|
|
5
|
-
private readonly sessionsSeen;
|
|
6
|
-
private latest;
|
|
7
|
-
constructor(capacity?: number);
|
|
8
|
-
/** Ingest a batch from a client. Tracks the session and advances latestSeq. */
|
|
9
|
-
ingest(sessionId: string, events: DiagEvent[]): void;
|
|
10
|
-
/** Answer an MCP query against the buffer. Newest-biased when limited. */
|
|
11
|
-
query(q?: RelayQuery): RelayResult;
|
|
12
|
-
size(): number;
|
|
13
|
-
}
|
package/dist/relay/store.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// The relay's memory: a bounded ring buffer of recent diagnostics events,
|
|
2
|
-
// queryable the way the MCP server asks ("network events since seq 40", "last
|
|
3
|
-
// 20 errors"). Pure and dependency-free so it is fully unit-testable; the WS
|
|
4
|
-
// server in server.ts is a thin wrapper that feeds this and answers queries
|
|
5
|
-
// from it.
|
|
6
|
-
export class EventStore {
|
|
7
|
-
capacity;
|
|
8
|
-
buf = [];
|
|
9
|
-
sessionsSeen = new Set();
|
|
10
|
-
latest = -1;
|
|
11
|
-
constructor(capacity = 5000) {
|
|
12
|
-
this.capacity = capacity;
|
|
13
|
-
}
|
|
14
|
-
/** Ingest a batch from a client. Tracks the session and advances latestSeq. */
|
|
15
|
-
ingest(sessionId, events) {
|
|
16
|
-
this.sessionsSeen.add(sessionId);
|
|
17
|
-
for (const e of events) {
|
|
18
|
-
// Tag ownership on the stored copy so cross-session queries can filter.
|
|
19
|
-
this.buf.push(withSession(e, sessionId));
|
|
20
|
-
if (e.seq > this.latest)
|
|
21
|
-
this.latest = e.seq;
|
|
22
|
-
}
|
|
23
|
-
// Ring: drop oldest beyond capacity.
|
|
24
|
-
if (this.buf.length > this.capacity) {
|
|
25
|
-
this.buf.splice(0, this.buf.length - this.capacity);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/** Answer an MCP query against the buffer. Newest-biased when limited. */
|
|
29
|
-
query(q = {}) {
|
|
30
|
-
let out = this.buf.filter((e) => {
|
|
31
|
-
if (q.sessionId && sessionOf(e) !== q.sessionId)
|
|
32
|
-
return false;
|
|
33
|
-
if (q.kind && e.kind !== q.kind)
|
|
34
|
-
return false;
|
|
35
|
-
if (q.since !== undefined && e.seq <= q.since)
|
|
36
|
-
return false;
|
|
37
|
-
return true;
|
|
38
|
-
});
|
|
39
|
-
if (q.limit !== undefined && out.length > q.limit) {
|
|
40
|
-
out = out.slice(out.length - q.limit); // keep the newest `limit`
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
events: out.map(stripSession),
|
|
44
|
-
latestSeq: this.latest,
|
|
45
|
-
sessions: [...this.sessionsSeen],
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
size() {
|
|
49
|
-
return this.buf.length;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// The store tags each event with its owning session out-of-band (a symbol key)
|
|
53
|
-
// so it survives in the buffer without polluting the wire type. Queries strip
|
|
54
|
-
// it before returning.
|
|
55
|
-
const SESSION = Symbol("session");
|
|
56
|
-
function withSession(e, sessionId) {
|
|
57
|
-
return Object.assign(Object.create(Object.getPrototypeOf(e)), e, {
|
|
58
|
-
[SESSION]: sessionId,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
function sessionOf(e) {
|
|
62
|
-
return e[SESSION];
|
|
63
|
-
}
|
|
64
|
-
function stripSession(e) {
|
|
65
|
-
const copy = { ...e };
|
|
66
|
-
delete copy[SESSION];
|
|
67
|
-
return copy;
|
|
68
|
-
}
|
package/dist/scaffold/index.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export declare const TEMPLATE_REPO = "git@github.com:vincentt-xr/v2-template.git";
|
|
2
|
-
/** Injectable git runner (real git by default; a fake in tests avoids the network). */
|
|
3
|
-
export type GitRunner = (args: string[]) => Promise<void>;
|
|
4
|
-
/**
|
|
5
|
-
* True when `dir` has no app yet (no package.json) → safe to scaffold into.
|
|
6
|
-
* A directory that already holds an app is left untouched (project_create just binds).
|
|
7
|
-
*/
|
|
8
|
-
export declare function needsScaffold(dir: string): Promise<boolean>;
|
|
9
|
-
export interface ScaffoldOptions {
|
|
10
|
-
repo?: string;
|
|
11
|
-
/** Test seam — defaults to spawning real git. */
|
|
12
|
-
runGit?: GitRunner;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Scaffold the template into `targetDir`. Clones to a temp dir, strips `.git`,
|
|
16
|
-
* copies the files in (never overwriting anything already there), then inits a
|
|
17
|
-
* fresh repo. Throws a clear error if the clone fails (e.g. no git access).
|
|
18
|
-
*/
|
|
19
|
-
export declare function scaffoldFromTemplate(targetDir: string, opts?: ScaffoldOptions): Promise<void>;
|
|
20
|
-
/**
|
|
21
|
-
* Install a freshly scaffolded project's dependencies so the first preview/build
|
|
22
|
-
* doesn't fail on a missing node_modules. Throws on failure (no network, no npm);
|
|
23
|
-
* the caller surfaces it as a "run npm install yourself" hint rather than aborting
|
|
24
|
-
* project creation.
|
|
25
|
-
*/
|
|
26
|
-
export declare function installDependencies(dir: string): Promise<void>;
|
package/dist/scaffold/index.js
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
// Scaffold a new app from the v2-template GitHub template as part of
|
|
2
|
-
// project_create. Shallow-clones the template, drops its git history, copies the
|
|
3
|
-
// files into the target, and inits a fresh repo the creator owns — the local-first
|
|
4
|
-
// equivalent of GitHub's "Use this template". Node-only (spawns git); not imported
|
|
5
|
-
// by browser consumers.
|
|
6
|
-
import { execFile } from "node:child_process";
|
|
7
|
-
import { promises as fs } from "node:fs";
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import { promisify } from "node:util";
|
|
11
|
-
const execFileAsync = promisify(execFile);
|
|
12
|
-
export const TEMPLATE_REPO = "git@github.com:vincentt-xr/v2-template.git";
|
|
13
|
-
const realGit = async (args) => {
|
|
14
|
-
await execFileAsync("git", args);
|
|
15
|
-
};
|
|
16
|
-
/**
|
|
17
|
-
* True when `dir` has no app yet (no package.json) → safe to scaffold into.
|
|
18
|
-
* A directory that already holds an app is left untouched (project_create just binds).
|
|
19
|
-
*/
|
|
20
|
-
export async function needsScaffold(dir) {
|
|
21
|
-
try {
|
|
22
|
-
await fs.access(path.join(dir, "package.json"));
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Scaffold the template into `targetDir`. Clones to a temp dir, strips `.git`,
|
|
31
|
-
* copies the files in (never overwriting anything already there), then inits a
|
|
32
|
-
* fresh repo. Throws a clear error if the clone fails (e.g. no git access).
|
|
33
|
-
*/
|
|
34
|
-
export async function scaffoldFromTemplate(targetDir, opts = {}) {
|
|
35
|
-
const repo = opts.repo ?? TEMPLATE_REPO;
|
|
36
|
-
const runGit = opts.runGit ?? realGit;
|
|
37
|
-
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "vincentt-tmpl-"));
|
|
38
|
-
try {
|
|
39
|
-
try {
|
|
40
|
-
await runGit(["clone", "--depth", "1", repo, tmp]);
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
-
throw new Error(`Could not clone the ${repo} template (check your git access): ${msg}`);
|
|
45
|
-
}
|
|
46
|
-
await fs.rm(path.join(tmp, ".git"), { recursive: true, force: true });
|
|
47
|
-
// Copy template contents in without clobbering anything already in the target.
|
|
48
|
-
for (const entry of await fs.readdir(tmp)) {
|
|
49
|
-
await fs.cp(path.join(tmp, entry), path.join(targetDir, entry), {
|
|
50
|
-
recursive: true,
|
|
51
|
-
force: false,
|
|
52
|
-
errorOnExist: false,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// Fresh history the creator owns (skip if the target is already a repo).
|
|
56
|
-
if (!(await isGitRepo(targetDir))) {
|
|
57
|
-
await runGit(["-C", targetDir, "init"]);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
await fs.rm(tmp, { recursive: true, force: true });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
async function isGitRepo(dir) {
|
|
65
|
-
try {
|
|
66
|
-
await execFileAsync("git", ["-C", dir, "rev-parse", "--git-dir"]);
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Install a freshly scaffolded project's dependencies so the first preview/build
|
|
75
|
-
* doesn't fail on a missing node_modules. Throws on failure (no network, no npm);
|
|
76
|
-
* the caller surfaces it as a "run npm install yourself" hint rather than aborting
|
|
77
|
-
* project creation.
|
|
78
|
-
*/
|
|
79
|
-
export async function installDependencies(dir) {
|
|
80
|
-
// Windows: npm is npm.cmd — execFile needs a shell to resolve it.
|
|
81
|
-
await execFileAsync("npm", ["install"], {
|
|
82
|
-
cwd: dir,
|
|
83
|
-
shell: process.platform === "win32",
|
|
84
|
-
});
|
|
85
|
-
}
|
package/dist/shared/config.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export interface MachineConfig {
|
|
2
|
-
/** Base URL of the Vincentt backend API (e.g. https://api.vincentt.studio). */
|
|
3
|
-
apiUrl: string;
|
|
4
|
-
/** Personal access token (bearer) for machine auth. */
|
|
5
|
-
pat: string;
|
|
6
|
-
}
|
|
7
|
-
export interface ProjectBinding {
|
|
8
|
-
/** The backend Project id this working tree publishes to. */
|
|
9
|
-
projectId: string;
|
|
10
|
-
/** The project's slug — its <slug>.<apex> host. Informational (server-authoritative). */
|
|
11
|
-
slug: string;
|
|
12
|
-
/** Optional per-project API override; else the machine config's apiUrl. */
|
|
13
|
-
apiUrl?: string;
|
|
14
|
-
}
|
|
15
|
-
export declare const MACHINE_CONFIG_PATH: string;
|
|
16
|
-
export declare function projectBindingPath(cwd: string): string;
|
|
17
|
-
export declare function loadMachineConfig(): Promise<Partial<MachineConfig>>;
|
|
18
|
-
export declare function loadProjectBinding(cwd: string): Promise<ProjectBinding | undefined>;
|
|
19
|
-
/**
|
|
20
|
-
* Write the per-tree binding and make sure `.vincentt/` is gitignored (so the
|
|
21
|
-
* secret-free-but-tenant-scoped binding never rides a commit or `git archive`).
|
|
22
|
-
*/
|
|
23
|
-
export declare function writeProjectBinding(cwd: string, binding: ProjectBinding): Promise<string>;
|
|
24
|
-
export interface ResolvedConfig {
|
|
25
|
-
apiUrl: string;
|
|
26
|
-
pat: string;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Resolve the API URL + PAT for a machine call. Precedence: a project binding's
|
|
30
|
-
* apiUrl wins for the URL; env vars override the machine-config file; the file is
|
|
31
|
-
* the base. Throws a clear, actionable error when no PAT is configured.
|
|
32
|
-
*/
|
|
33
|
-
export declare function resolveConfig(cwd: string): Promise<ResolvedConfig>;
|
package/dist/shared/config.js
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Config the lifecycle MCP verbs (project_create / project_publish) read to reach
|
|
2
|
-
// a creator's Vincentt backend. Two homes, by sensitivity:
|
|
3
|
-
// • ~/.vincentt/config.json — machine-global { apiUrl, pat }. The PAT is a
|
|
4
|
-
// bearer credential, so it lives OUTSIDE any repo (never committed).
|
|
5
|
-
// • <project>/.vincentt/project.json — the per-tree binding { projectId, slug }.
|
|
6
|
-
// Gitignored: `git archive HEAD` (the remix path) must NOT carry it, or a
|
|
7
|
-
// remixer would inherit — and publish over — the original's project.
|
|
8
|
-
// Env vars (VINCENTT_API_URL / VINCENTT_PAT) override the file for local/dogfood
|
|
9
|
-
// runs so a creator can point at a dev backend without editing the machine config.
|
|
10
|
-
import { promises as fs } from "node:fs";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
export const MACHINE_CONFIG_PATH = path.join(os.homedir(), ".vincentt", "config.json");
|
|
14
|
-
const BINDING_DIR = ".vincentt";
|
|
15
|
-
const BINDING_FILE = "project.json";
|
|
16
|
-
export function projectBindingPath(cwd) {
|
|
17
|
-
return path.join(cwd, BINDING_DIR, BINDING_FILE);
|
|
18
|
-
}
|
|
19
|
-
async function readJsonIfExists(p) {
|
|
20
|
-
try {
|
|
21
|
-
return JSON.parse(await fs.readFile(p, "utf8"));
|
|
22
|
-
}
|
|
23
|
-
catch (err) {
|
|
24
|
-
if (err.code === "ENOENT")
|
|
25
|
-
return undefined;
|
|
26
|
-
throw err;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
export async function loadMachineConfig() {
|
|
30
|
-
return (await readJsonIfExists(MACHINE_CONFIG_PATH)) ?? {};
|
|
31
|
-
}
|
|
32
|
-
export async function loadProjectBinding(cwd) {
|
|
33
|
-
return readJsonIfExists(projectBindingPath(cwd));
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Write the per-tree binding and make sure `.vincentt/` is gitignored (so the
|
|
37
|
-
* secret-free-but-tenant-scoped binding never rides a commit or `git archive`).
|
|
38
|
-
*/
|
|
39
|
-
export async function writeProjectBinding(cwd, binding) {
|
|
40
|
-
await fs.mkdir(path.join(cwd, BINDING_DIR), { recursive: true });
|
|
41
|
-
const p = projectBindingPath(cwd);
|
|
42
|
-
await fs.writeFile(p, JSON.stringify(binding, null, 2) + "\n", "utf8");
|
|
43
|
-
await ensureGitignored(cwd, `${BINDING_DIR}/`);
|
|
44
|
-
return p;
|
|
45
|
-
}
|
|
46
|
-
/** Append a pattern to the repo's .gitignore if not already present (best-effort). */
|
|
47
|
-
async function ensureGitignored(cwd, pattern) {
|
|
48
|
-
const gitignore = path.join(cwd, ".gitignore");
|
|
49
|
-
try {
|
|
50
|
-
const current = await fs.readFile(gitignore, "utf8").catch(() => "");
|
|
51
|
-
const has = current.split(/\r?\n/).some((l) => l.trim() === pattern.trim());
|
|
52
|
-
if (has)
|
|
53
|
-
return;
|
|
54
|
-
const next = current && !current.endsWith("\n") ? `${current}\n${pattern}\n` : `${current}${pattern}\n`;
|
|
55
|
-
await fs.writeFile(gitignore, next, "utf8");
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
// A missing/unwritable .gitignore must not fail project creation.
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Resolve the API URL + PAT for a machine call. Precedence: a project binding's
|
|
63
|
-
* apiUrl wins for the URL; env vars override the machine-config file; the file is
|
|
64
|
-
* the base. Throws a clear, actionable error when no PAT is configured.
|
|
65
|
-
*/
|
|
66
|
-
export async function resolveConfig(cwd) {
|
|
67
|
-
const machine = await loadMachineConfig();
|
|
68
|
-
const binding = await loadProjectBinding(cwd);
|
|
69
|
-
const apiUrl = binding?.apiUrl ?? process.env.VINCENTT_API_URL ?? machine.apiUrl ?? "http://localhost:5051";
|
|
70
|
-
const pat = process.env.VINCENTT_PAT ?? machine.pat;
|
|
71
|
-
if (!pat) {
|
|
72
|
-
throw new Error(`No Vincentt access token found. Add {"apiUrl":"…","pat":"…"} to ${MACHINE_CONFIG_PATH}, ` +
|
|
73
|
-
`or set the VINCENTT_PAT env var.`);
|
|
74
|
-
}
|
|
75
|
-
return { apiUrl: apiUrl.replace(/\/$/, ""), pat };
|
|
76
|
-
}
|