@genex-ai/cli-demo 1.15.0-dev.577 → 1.15.1-dev.578

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.
@@ -7,7 +7,7 @@ import {
7
7
  isRenderMode,
8
8
  sceneSummary,
9
9
  sheetOf
10
- } from "./chunk-YSX7SJFM.js";
10
+ } from "./chunk-JGX6YRC7.js";
11
11
  import {
12
12
  getCliVersion
13
13
  } from "./chunk-HYCSNWYX.js";
@@ -4,11 +4,304 @@ import {
4
4
  ENV_TOKEN_KEY,
5
5
  c,
6
6
  getApiUrl,
7
+ getAuthUrl,
7
8
  getCliVersion,
8
9
  getGenexEnvPath
9
10
  } from "./chunk-HYCSNWYX.js";
10
11
 
12
+ // src/lib/terms.ts
13
+ import readline from "readline";
14
+
15
+ // src/lib/source-sync.ts
16
+ import fs from "fs/promises";
17
+ import path from "path";
18
+ import os from "os";
19
+
20
+ // src/utils/run.ts
21
+ import { spawn } from "child_process";
22
+ var WIN_SHELL_COMMANDS = /* @__PURE__ */ new Set(["npm", "npx"]);
23
+ function run(cmd, args, env) {
24
+ const shell = process.platform === "win32" && WIN_SHELL_COMMANDS.has(cmd);
25
+ return new Promise((resolve) => {
26
+ let child;
27
+ try {
28
+ child = spawn(cmd, args, { env: env ? { ...process.env, ...env } : process.env, shell });
29
+ } catch {
30
+ resolve({ code: -1, out: "", err: `${cmd} not found` });
31
+ return;
32
+ }
33
+ let out = "";
34
+ let err = "";
35
+ child.stdout?.on("data", (d) => out += String(d));
36
+ child.stderr?.on("data", (d) => err += String(d));
37
+ child.on("error", () => resolve({ code: -1, out, err: `${cmd} not found` }));
38
+ child.on("close", (code) => resolve({ code: code ?? -1, out, err }));
39
+ });
40
+ }
41
+
42
+ // src/lib/source-sync.ts
43
+ async function readRemoteSource(apiUrl, token, projectId) {
44
+ try {
45
+ const res = await apiFetch(`${apiUrl}/api/projects/${projectId}`, {
46
+ headers: { Authorization: `Bearer ${token}` }
47
+ });
48
+ if (!res.ok) return null;
49
+ const data = await res.json().catch(() => null);
50
+ if (!data?.project) return null;
51
+ return {
52
+ stagingCommit: data.project.stagingCommitSha ?? null,
53
+ commit: data.project.commitSha ?? null
54
+ };
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function isStale(local, remote) {
60
+ if (!local || !remote) return false;
61
+ return local !== remote;
62
+ }
63
+ function inHostedSession() {
64
+ return process.env.GENEX_HOSTED_SESSION === "1";
65
+ }
66
+ function mayForceOverAnotherDevice() {
67
+ return !inHostedSession();
68
+ }
69
+ function reportForceRefused(log) {
70
+ log.error("`--force` is not available inside a Genex chat session.");
71
+ log.dim(" It would replace work shipped from another machine, and that is not yours to discard here.");
72
+ log.dim(` Do this instead \u2014 it keeps both sides:`);
73
+ log.dim(` 1. ${c.cyan("npx genex pull --force")} takes the other machine's work (a copy of this folder is kept under .genex/replaced-*)`);
74
+ log.dim(` 2. redo your change on top of it \u2014 you know what you just changed`);
75
+ log.dim(` 3. ${c.cyan("npx genex preview")}`);
76
+ log.dim(" If the user explicitly wants their chat version to win, ask them to run `npx genex preview --force` themselves.");
77
+ }
78
+ function reportStale(log, slug, local, remote) {
79
+ log.error(`${c.cyan(slug)} was updated from another device \u2014 nothing was deployed.`);
80
+ if (remote && local) {
81
+ const short = remote.slice(0, 7) !== local.slice(0, 7);
82
+ const r = short ? remote.slice(0, 7) : remote;
83
+ const l = short ? local.slice(0, 7) : local;
84
+ log.dim(` the draft is on ${r}, this folder last shipped ${l}`);
85
+ }
86
+ log.dim(` ${c.cyan("npx genex pull")} \u2014 take the other device's work (refuses if you have unshipped changes)`);
87
+ log.dim(` ${c.cyan("npx genex preview --force")} \u2014 keep yours and replace theirs`);
88
+ }
89
+ async function sourceTreeHash(cwd) {
90
+ const gitDir = await fs.mkdtemp(path.join(os.tmpdir(), "genex-tree-"));
91
+ const base = { GIT_DIR: gitDir };
92
+ try {
93
+ if ((await run("git", ["init", "-q"], base)).code !== 0) return null;
94
+ await fs.writeFile(
95
+ path.join(gitDir, "info", "exclude"),
96
+ ["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
97
+ );
98
+ if ((await run("git", ["lfs", "version"], base)).code === 0) {
99
+ const filters = [
100
+ ["filter.lfs.clean", "git-lfs clean -- %f"],
101
+ ["filter.lfs.smudge", "git-lfs smudge -- %f"],
102
+ ["filter.lfs.process", "git-lfs filter-process"],
103
+ ["filter.lfs.required", "true"]
104
+ ];
105
+ for (const [key, value] of filters) await run("git", ["config", key, value], base);
106
+ }
107
+ const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path.join(gitDir, "index-tree") };
108
+ if ((await run("git", ["add", "-A"], env)).code !== 0) return null;
109
+ const tree = (await run("git", ["write-tree"], env)).out.trim();
110
+ return /^[0-9a-f]{40}$/.test(tree) ? tree : null;
111
+ } catch {
112
+ return null;
113
+ } finally {
114
+ await fs.rm(gitDir, { recursive: true, force: true }).catch(() => {
115
+ });
116
+ }
117
+ }
118
+ function urlHasEmbeddedCredentials(url) {
119
+ return /^[a-z][a-z0-9+.-]*:\/\/[^/@]+@/i.test(url);
120
+ }
121
+ function credentialHelperOff(url) {
122
+ if (!urlHasEmbeddedCredentials(url)) return {};
123
+ return { GIT_CONFIG_COUNT: "1", GIT_CONFIG_KEY_0: "credential.helper", GIT_CONFIG_VALUE_0: "" };
124
+ }
125
+ async function fetchCloneGrant(apiUrl, token, projectId, log) {
126
+ let res;
127
+ try {
128
+ res = await apiFetch(`${apiUrl}/api/projects/${projectId}/push-token`, {
129
+ method: "POST",
130
+ headers: { Authorization: `Bearer ${token}`, "X-Genex-Source-Intent": "read" }
131
+ });
132
+ } catch (err) {
133
+ log.error(`Couldn't reach the API to authorize the source read: ${String(err)}`);
134
+ return null;
135
+ }
136
+ if (res.status === 401) {
137
+ log.error("Not authorized \u2014 your token may have expired. Re-run `genex auth`.");
138
+ return null;
139
+ }
140
+ if (!res.ok) {
141
+ log.error(`Couldn't authorize the source read (HTTP ${res.status}).`);
142
+ return null;
143
+ }
144
+ const data = await res.json().catch(() => null);
145
+ const url = data?.pushUrl ?? data?.cloneUrl;
146
+ if (!url) {
147
+ log.error("The API didn't return a source URL.");
148
+ return null;
149
+ }
150
+ return { cloneUrl: url, sourceRef: data?.sourceRef ?? null };
151
+ }
152
+ async function cloneSource(grant, dest, log) {
153
+ const args = grant.sourceRef ? ["clone", "--branch", grant.sourceRef, grant.cloneUrl, dest] : ["clone", grant.cloneUrl, dest];
154
+ const env = {
155
+ GIT_LFS_SKIP_SMUDGE: "1",
156
+ GIT_TERMINAL_PROMPT: "0",
157
+ ...credentialHelperOff(grant.cloneUrl)
158
+ };
159
+ const cloned = await run("git", args, env);
160
+ if (cloned.code !== 0) {
161
+ log.error("Couldn't download the game's source.");
162
+ log.dim(` git clone exited ${cloned.code}`);
163
+ return false;
164
+ }
165
+ const repo = { ...env, GIT_DIR: path.join(dest, ".git"), GIT_WORK_TREE: dest };
166
+ const filters = [
167
+ ["filter.lfs.clean", "git-lfs clean -- %f"],
168
+ ["filter.lfs.smudge", "git-lfs smudge -- %f"],
169
+ ["filter.lfs.process", "git-lfs filter-process"],
170
+ ["filter.lfs.required", "true"]
171
+ ];
172
+ for (const [key, value] of filters) await run("git", ["config", key, value], repo);
173
+ const pulled = await run("git", ["lfs", "pull"], repo);
174
+ if (pulled.code !== 0) {
175
+ log.warn("Binary assets (models, textures, audio) are still pointer files \u2014 `git lfs pull` failed.");
176
+ log.dim(" The source is there; run `git lfs pull` in this folder once git-lfs works.");
177
+ }
178
+ return true;
179
+ }
180
+
181
+ // src/lib/terms.ts
182
+ var TERMS_ERROR_CODE = "terms_acceptance_required";
183
+ var TERMS_WAIT_MS = 1e5;
184
+ var TERMS_POLL_MS = 3e3;
185
+ async function isTermsRefusal(res) {
186
+ if (res.status !== 403) return false;
187
+ try {
188
+ const body = await res.clone().json();
189
+ return body?.error === TERMS_ERROR_CODE;
190
+ } catch {
191
+ return false;
192
+ }
193
+ }
194
+ async function termsRefusalUrl(res) {
195
+ try {
196
+ const body = await res.clone().json();
197
+ return typeof body?.url === "string" && body.url ? body.url : null;
198
+ } catch {
199
+ return null;
200
+ }
201
+ }
202
+ function acceptUrl(fromServer, authUrl) {
203
+ return fromServer || `${getAuthUrl(authUrl)}/accept`;
204
+ }
205
+ function reportTermsRefusal(log, url) {
206
+ log.error("Your account needs to accept the updated Terms before it can generate or publish.");
207
+ log.dim(" Nothing was charged, and nothing on your project changed.");
208
+ log.dim(` Open ${c.cyan(acceptUrl(url))} and agree \u2014 one click.`);
209
+ }
210
+ async function waitForAcceptance(opts) {
211
+ const timeoutMs = opts.timeoutMs ?? TERMS_WAIT_MS;
212
+ const intervalMs = opts.intervalMs ?? TERMS_POLL_MS;
213
+ const doFetch = opts.fetchImpl ?? fetch;
214
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
215
+ const now = opts.now ?? Date.now;
216
+ const base = opts.apiUrl.replace(/\/+$/, "");
217
+ opts.log.dim(` Waiting for you to accept (up to ${Math.round(timeoutMs / 1e3)}s)\u2026`);
218
+ const deadline = now() + timeoutMs;
219
+ let first = true;
220
+ while (first || now() < deadline) {
221
+ if (!first) await sleep(Math.min(intervalMs, Math.max(0, deadline - now())));
222
+ first = false;
223
+ try {
224
+ const res = await doFetch(`${base}/api/legal/status`, {
225
+ headers: { Authorization: `Bearer ${opts.token}` }
226
+ });
227
+ if (!res.ok) return false;
228
+ const body = await res.json().catch(() => null);
229
+ if (body?.accepted === true) {
230
+ opts.log.success("Accepted \u2014 continuing.");
231
+ return true;
232
+ }
233
+ } catch {
234
+ return false;
235
+ }
236
+ }
237
+ opts.log.error("Still not accepted. Re-run this command once you have agreed.");
238
+ return false;
239
+ }
240
+ function askOnStdin(question) {
241
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
242
+ return new Promise((resolve) => {
243
+ rl.question(question, (answer) => {
244
+ rl.close();
245
+ resolve(answer);
246
+ });
247
+ });
248
+ }
249
+ async function runAccept(opts) {
250
+ const { token, log } = opts;
251
+ const interactive = opts.interactive ?? Boolean(process.stdin.isTTY);
252
+ const apiUrl = getApiUrl(opts.apiUrl);
253
+ let acceptPage = null;
254
+ const status = await apiFetch(`${apiUrl}/api/legal/status`, {
255
+ headers: { Authorization: `Bearer ${token}` }
256
+ }).catch(() => null);
257
+ if (status?.ok) {
258
+ const body = await status.json().catch(() => null);
259
+ if (body?.accepted) {
260
+ log.success("Already accepted \u2014 nothing to do.");
261
+ return true;
262
+ }
263
+ acceptPage = typeof body?.acceptUrl === "string" ? body.acceptUrl : null;
264
+ log.plain("Before generating or publishing, please read and agree to:");
265
+ for (const doc of body?.documents ?? []) log.plain(` ${doc.title} ${c.cyan(doc.url)}`);
266
+ log.plain("");
267
+ }
268
+ if (!interactive) {
269
+ log.plain("Accepting the Terms needs a person \u2014 an agent cannot agree on your behalf.");
270
+ log.plain(` Open ${c.cyan(acceptUrl(acceptPage))} and agree \u2014 one click.`);
271
+ return waitForAcceptance({ apiUrl, token, log, ...opts.wait ?? {} });
272
+ }
273
+ const ask = opts.ask ?? askOnStdin;
274
+ const answer = (await ask("Type 'agree' to accept: ")).trim().toLowerCase();
275
+ if (answer !== "agree") {
276
+ log.error("Not accepted \u2014 nothing was recorded.");
277
+ return false;
278
+ }
279
+ const res = await apiFetch(`${apiUrl}/api/legal/accept`, {
280
+ method: "POST",
281
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
282
+ body: JSON.stringify({ surface: "cli" })
283
+ }).catch(() => null);
284
+ if (!res?.ok) {
285
+ log.error(`Couldn't record your acceptance (HTTP ${res?.status ?? "no response"}).`);
286
+ return false;
287
+ }
288
+ log.success("Accepted. Re-run your command.");
289
+ return true;
290
+ }
291
+
11
292
  // src/lib/api.ts
293
+ var stderrLogger = /* @__PURE__ */ (() => {
294
+ const err = (s) => void process.stderr.write(s + "\n");
295
+ return {
296
+ info: err,
297
+ success: (m) => err(`${c.green("\u2713")} ${m}`),
298
+ warn: (m) => err(`${c.yellow("!")} ${m}`),
299
+ error: (m) => err(`${c.red("\u2717")} ${m}`),
300
+ step: err,
301
+ dim: (m) => err(c.dim(m)),
302
+ plain: err
303
+ };
304
+ })();
12
305
  var CLI_VERSION_HEADER = "x-genex-cli-version";
13
306
  var WORKSPACE_HEADER = "x-genex-workspace";
14
307
  function workspaceHeaderValue(label) {
@@ -54,13 +347,41 @@ var structuredPrinted = /* @__PURE__ */ new WeakSet();
54
347
  function printedStructuredError(res) {
55
348
  return structuredPrinted.has(res);
56
349
  }
57
- async function apiFetch(url, init = {}) {
350
+ var termsWaitOverride = null;
351
+ function replayable(body) {
352
+ if (body === void 0 || body === null) return true;
353
+ if (typeof body === "string") return true;
354
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer) return true;
355
+ if (typeof FormData !== "undefined" && body instanceof FormData) return true;
356
+ if (body instanceof URLSearchParams) return true;
357
+ if (typeof Blob !== "undefined" && body instanceof Blob) return true;
358
+ return false;
359
+ }
360
+ async function apiFetch(url, init = {}, opts = {}) {
58
361
  const headers = new Headers(init.headers);
59
362
  if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
60
363
  if (workspaceLabel && !headers.has(WORKSPACE_HEADER)) {
61
364
  headers.set(WORKSPACE_HEADER, workspaceLabel);
62
365
  }
63
366
  const res = await fetch(url, { ...init, headers });
367
+ if (res.status === 403 && !opts.noTermsWait && await isTermsRefusal(res)) {
368
+ const log = stderrLogger;
369
+ reportTermsRefusal(log, await termsRefusalUrl(res));
370
+ const auth = headers.get("authorization") ?? headers.get("Authorization");
371
+ const token = auth?.replace(/^Bearer\s+/i, "").trim();
372
+ let accepted = false;
373
+ if (token && replayable(init.body)) {
374
+ accepted = await waitForAcceptance({
375
+ apiUrl: new URL(url).origin,
376
+ token,
377
+ log,
378
+ ...termsWaitOverride ?? {}
379
+ });
380
+ }
381
+ if (accepted) return apiFetch(url, init, { ...opts, noTermsWait: true });
382
+ structuredPrinted.add(res);
383
+ return res;
384
+ }
64
385
  if (res.status === 426) {
65
386
  try {
66
387
  const body = await res.clone().json();
@@ -119,8 +440,8 @@ async function fetchSignedInEmail(apiUrl, token) {
119
440
  }
120
441
 
121
442
  // src/lib/blender-client.ts
122
- import fs from "fs";
123
- import path from "path";
443
+ import fs2 from "fs";
444
+ import path2 from "path";
124
445
  var BLENDER_TIMEOUT_MS = 15 * 60 * 1e3;
125
446
  var RENDER_MODES = ["solid", "wireframe", "normals", "lit"];
126
447
  function isRenderMode(v) {
@@ -153,19 +474,19 @@ function sheetOf(r) {
153
474
  function sheetExt(mime) {
154
475
  return mime === "image/webp" ? "webp" : "png";
155
476
  }
156
- var SEAT_FILE = path.join(".genex", "blender-seat.json");
477
+ var SEAT_FILE = path2.join(".genex", "blender-seat.json");
157
478
  function readSeatGrant(cwd = process.cwd()) {
158
479
  try {
159
- const raw = JSON.parse(fs.readFileSync(path.join(cwd, SEAT_FILE), "utf8"));
480
+ const raw = JSON.parse(fs2.readFileSync(path2.join(cwd, SEAT_FILE), "utf8"));
160
481
  return typeof raw.url === "string" && typeof raw.token === "string" ? { url: raw.url, token: raw.token } : null;
161
482
  } catch {
162
483
  return null;
163
484
  }
164
485
  }
165
486
  function writeSeatGrant(grant, cwd = process.cwd()) {
166
- const file = path.join(cwd, SEAT_FILE);
167
- fs.mkdirSync(path.dirname(file), { recursive: true });
168
- fs.writeFileSync(file, JSON.stringify(grant, null, 2) + "\n", { mode: 384 });
487
+ const file = path2.join(cwd, SEAT_FILE);
488
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
489
+ fs2.writeFileSync(file, JSON.stringify(grant, null, 2) + "\n", { mode: 384 });
169
490
  }
170
491
  async function blenderCall(base, route, body) {
171
492
  const seat = readSeatGrant();
@@ -230,18 +551,18 @@ function sceneSummary(s) {
230
551
  }
231
552
 
232
553
  // src/lib/store.ts
233
- import fs3 from "fs/promises";
234
- import path3 from "path";
554
+ import fs4 from "fs/promises";
555
+ import path4 from "path";
235
556
 
236
557
  // src/lib/env.ts
237
- import fs2 from "fs/promises";
238
- import path2 from "path";
239
- import { spawn } from "child_process";
558
+ import fs3 from "fs/promises";
559
+ import path3 from "path";
560
+ import { spawn as spawn2 } from "child_process";
240
561
  async function writeEnvVar(envPath, key, value) {
241
562
  let content = "";
242
563
  let existed = false;
243
564
  try {
244
- content = await fs2.readFile(envPath, "utf8");
565
+ content = await fs3.readFile(envPath, "utf8");
245
566
  existed = true;
246
567
  } catch {
247
568
  }
@@ -261,14 +582,14 @@ async function writeEnvVar(envPath, key, value) {
261
582
  next = prefix + assignment + "\n";
262
583
  mode = existed ? "appended" : "created";
263
584
  }
264
- await fs2.mkdir(path2.dirname(envPath), { recursive: true });
265
- await fs2.writeFile(envPath, next, { mode: 384 });
585
+ await fs3.mkdir(path3.dirname(envPath), { recursive: true });
586
+ await fs3.writeFile(envPath, next, { mode: 384 });
266
587
  await restrictFilePermissions(envPath);
267
588
  return { mode, path: envPath };
268
589
  }
269
590
  async function restrictFilePermissions(filePath) {
270
591
  if (process.platform !== "win32") {
271
- await fs2.chmod(filePath, 384).catch(() => {
592
+ await fs3.chmod(filePath, 384).catch(() => {
272
593
  });
273
594
  return;
274
595
  }
@@ -276,7 +597,7 @@ async function restrictFilePermissions(filePath) {
276
597
  if (!user) return;
277
598
  await new Promise((resolve) => {
278
599
  try {
279
- const child = spawn(
600
+ const child = spawn2(
280
601
  "icacls",
281
602
  [filePath, "/inheritance:r", "/grant:r", `${user}:F`],
282
603
  { stdio: "ignore" }
@@ -300,14 +621,14 @@ function escapeRegExp(s) {
300
621
 
301
622
  // src/lib/store.ts
302
623
  function getProjectMetadataPath(cwd = process.cwd()) {
303
- return path3.join(cwd, ".genex", "project.json");
624
+ return path4.join(cwd, ".genex", "project.json");
304
625
  }
305
626
  function getWorkspacePath(cwd = process.cwd()) {
306
- return path3.join(cwd, ".genex", "workspace.json");
627
+ return path4.join(cwd, ".genex", "workspace.json");
307
628
  }
308
629
  async function readWorkspace(cwd = process.cwd()) {
309
630
  try {
310
- const raw = await fs3.readFile(getWorkspacePath(cwd), "utf8");
631
+ const raw = await fs4.readFile(getWorkspacePath(cwd), "utf8");
311
632
  return JSON.parse(raw);
312
633
  } catch {
313
634
  return null;
@@ -315,9 +636,9 @@ async function readWorkspace(cwd = process.cwd()) {
315
636
  }
316
637
  async function writeWorkspace(meta, cwd = process.cwd()) {
317
638
  const file = getWorkspacePath(cwd);
318
- await fs3.mkdir(path3.dirname(file), { recursive: true });
319
- await fs3.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
320
- await fs3.chmod(file, 384).catch(() => {
639
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
640
+ await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
641
+ await fs4.chmod(file, 384).catch(() => {
321
642
  });
322
643
  return { path: file };
323
644
  }
@@ -330,7 +651,7 @@ async function rotateRejectedEnv(envPath) {
330
651
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
331
652
  const aside = `${file}.rejected-${stamp}`;
332
653
  try {
333
- await fs3.rename(file, aside);
654
+ await fs4.rename(file, aside);
334
655
  return aside;
335
656
  } catch {
336
657
  return null;
@@ -340,14 +661,14 @@ async function readUserToken(envPath) {
340
661
  const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
341
662
  if (fromGenex) return fromGenex;
342
663
  if (!envPath && !process.env[ENV_FILE_ENV]) {
343
- return readTokenFromFile(path3.join(process.cwd(), ".env"));
664
+ return readTokenFromFile(path4.join(process.cwd(), ".env"));
344
665
  }
345
666
  return null;
346
667
  }
347
668
  async function readTokenFromFile(file) {
348
669
  let content;
349
670
  try {
350
- content = await fs3.readFile(file, "utf8");
671
+ content = await fs4.readFile(file, "utf8");
351
672
  } catch {
352
673
  return null;
353
674
  }
@@ -363,7 +684,7 @@ function stripQuotes(v) {
363
684
  }
364
685
  async function readProject(cwd = process.cwd()) {
365
686
  try {
366
- const raw = await fs3.readFile(getProjectMetadataPath(cwd), "utf8");
687
+ const raw = await fs4.readFile(getProjectMetadataPath(cwd), "utf8");
367
688
  return JSON.parse(raw);
368
689
  } catch {
369
690
  return null;
@@ -371,9 +692,9 @@ async function readProject(cwd = process.cwd()) {
371
692
  }
372
693
  async function writeProject(meta, cwd = process.cwd()) {
373
694
  const file = getProjectMetadataPath(cwd);
374
- await fs3.mkdir(path3.dirname(file), { recursive: true });
375
- await fs3.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
376
- await fs3.chmod(file, 384).catch(() => {
695
+ await fs4.mkdir(path4.dirname(file), { recursive: true });
696
+ await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
697
+ await fs4.chmod(file, 384).catch(() => {
377
698
  });
378
699
  return { path: file };
379
700
  }
@@ -429,6 +750,21 @@ async function acquireSeat(opts) {
429
750
  }
430
751
 
431
752
  export {
753
+ run,
754
+ readRemoteSource,
755
+ isStale,
756
+ inHostedSession,
757
+ mayForceOverAnotherDevice,
758
+ reportForceRefused,
759
+ reportStale,
760
+ sourceTreeHash,
761
+ urlHasEmbeddedCredentials,
762
+ fetchCloneGrant,
763
+ cloneSource,
764
+ isTermsRefusal,
765
+ acceptUrl,
766
+ reportTermsRefusal,
767
+ runAccept,
432
768
  setWorkspaceHeader,
433
769
  printedStructuredError,
434
770
  apiFetch,