@avocadostudio-ai/orchestrator-core 0.4.0 → 0.5.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.
@@ -4,7 +4,8 @@
4
4
  */
5
5
  import { appendFileSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
- const LOG_PATH = resolve(process.cwd(), "../../.data/agent-log.ndjson");
7
+ import { resolveDataDir } from "../state/data-dir.js";
8
+ const LOG_PATH = resolve(resolveDataDir(), "agent-log.ndjson");
8
9
  export function logAgent(streamId, event, detail, startedAt) {
9
10
  const entry = {
10
11
  ts: Date.now(),
@@ -51,6 +51,7 @@ import { createCmsBootstrapCache } from "../cms/bootstrap.js";
51
51
  import { resolveCapabilities } from "../cms/adapter.js";
52
52
  import { mediaSourceFromUnknown } from "../cms/media-sources.js";
53
53
  import { isAccessGateEnabled, mintAccessToken, verifyAccessPassword } from "../http/access-tokens.js";
54
+ import { declareLibraryMount, observeLibraryMount } from "./library-mount.js";
54
55
  import { checkAuth, resolveAuth } from "./auth.js";
55
56
  import { setSiteAssetLister, invalidateSiteAssets } from "../state/site-assets.js";
56
57
  const defaultModelLookup = () => ({
@@ -416,16 +417,40 @@ export function createOrchestrator(config = {}) {
416
417
  const gateLogger = config.logger ?? consoleLogger();
417
418
  {
418
419
  const resolved = resolveAuth(config.auth);
419
- const line = `[auth] library mode: ${resolved.mode} — ${resolved.reason}`;
420
+ /*
421
+ * `next build` evaluates this route module to collect its exports, with
422
+ * `NODE_ENV=production` and none of the deployment's environment. With no
423
+ * credential configured the gate resolves to `closed` and this printed a
424
+ * red `[error]` in the middle of an otherwise clean, *successful* build —
425
+ * about a request that is not being served, on a machine that is not the
426
+ * deployment.
427
+ *
428
+ * It is still the only notice anyone gets of a real problem, so it is not
429
+ * silenced: during a build it is a warning that says which state it is
430
+ * describing. At runtime it stays an error, because then it means every
431
+ * request is actually being refused.
432
+ */
433
+ const building = process.env.NEXT_PHASE === "phase-production-build";
434
+ const line = building
435
+ ? `[auth] library mode: ${resolved.mode} — ${resolved.reason} ` +
436
+ `(evaluated during the build; set ACCESS_PASSWORD_HASH or ` +
437
+ `ORCHESTRATOR_ACCESS_TOKEN in the deployment's environment, not here)`
438
+ : `[auth] library mode: ${resolved.mode} — ${resolved.reason}`;
420
439
  if (resolved.mode === "closed")
421
- gateLogger.error(line);
440
+ building ? gateLogger.warn(line) : gateLogger.error(line);
422
441
  else if (resolved.mode === "open-dev")
423
442
  gateLogger.warn(line);
424
443
  else
425
444
  gateLogger.info(line);
426
445
  }
446
+ // Tell the SDK's draft fetch where we are, so it stops defaulting to the
447
+ // standalone orchestrator on :4200 that a library-mode site does not run.
448
+ // See `library-mount.ts` for why this is not simply a config value.
449
+ if (config.previewUrl)
450
+ declareLibraryMount(`${config.previewUrl.replace(/\/+$/, "")}${basePath}`);
427
451
  const handler = async function handler(request) {
428
452
  const url = new URL(request.url);
453
+ observeLibraryMount(`${url.origin}${basePath}`);
429
454
  const path = stripBasePath(url.pathname, basePath);
430
455
  const cors = corsHeadersFor(request, config);
431
456
  if (request.method === "OPTIONS") {
@@ -0,0 +1,6 @@
1
+ export declare const LIBRARY_MOUNT_KEY = "__avocado_library_mount__";
2
+ /** Called at construction, from `previewUrl` + `basePath`. */
3
+ export declare function declareLibraryMount(url: string): void;
4
+ /** Called on each request, from the URL the runtime actually received. */
5
+ export declare function observeLibraryMount(url: string): void;
6
+ export declare function getLibraryMount(): string | null;
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Where this process's own orchestrator is mounted, if it has one.
3
+ *
4
+ * In library mode the orchestrator is a route inside the site's own Next app,
5
+ * and the SDK's draft fetch had no way to know that: with `ORCHESTRATOR_URL`
6
+ * unset it fell back to `http://127.0.0.1:4200`, the *standalone* orchestrator,
7
+ * which a library-mode site by definition does not run. The failure is silent
8
+ * when nothing is listening there (the draft fetch fails, the preview falls back
9
+ * to published content, and every edit appears to do nothing) and worse when
10
+ * something is — the fetch succeeds against a foreign process and the preview
11
+ * renders another project's pages. Anyone integrating is likely to have a :4200
12
+ * up, because that is what the standalone stack runs on.
13
+ *
14
+ * So the handler records its own address, and `getOrchestratorUrl()` in the SDK
15
+ * reads it through the same `globalThis` key. The key is a string on both sides
16
+ * rather than an import because `@avocadostudio-ai/orchestrator-core` is an
17
+ * *optional* peer of the SDK — a site that is not in library mode does not have
18
+ * it installed, and neither package may require the other.
19
+ *
20
+ * Two sources, in order of trust:
21
+ * - the origin of a real request this handler served, which cannot be wrong;
22
+ * - `config.previewUrl + basePath` at construction time, which is available
23
+ * before any request and is what covers a preview page that renders before
24
+ * the orchestrator route module has ever been evaluated.
25
+ */
26
+ export const LIBRARY_MOUNT_KEY = "__avocado_library_mount__";
27
+ function slot() {
28
+ const g = globalThis;
29
+ return g[LIBRARY_MOUNT_KEY] ?? (g[LIBRARY_MOUNT_KEY] = {});
30
+ }
31
+ /** Called at construction, from `previewUrl` + `basePath`. */
32
+ export function declareLibraryMount(url) {
33
+ slot().declared = url.replace(/\/+$/, "");
34
+ }
35
+ /** Called on each request, from the URL the runtime actually received. */
36
+ export function observeLibraryMount(url) {
37
+ const s = slot();
38
+ const next = url.replace(/\/+$/, "");
39
+ if (s.observed !== next)
40
+ s.observed = next;
41
+ }
42
+ export function getLibraryMount() {
43
+ const s = slot();
44
+ return s.observed ?? s.declared ?? null;
45
+ }
@@ -1,6 +1,7 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import OpenAI from "openai";
4
+ import { resolveDataDir } from "../state/data-dir.js";
4
5
  import { listImages, fileNameToAlt, resolveGdriveFolderId } from "./gdrive-client.js";
5
6
  // ---------------------------------------------------------------------------
6
7
  // Image generation timing — rolling average for progress estimation
@@ -218,7 +219,7 @@ export async function generateVariationImageWithOpenAI(args) {
218
219
  const background = args.background ?? "auto";
219
220
  const outputFormat = args.outputFormat ?? "png";
220
221
  const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
221
- const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(process.cwd(), "../../.data/generated-images");
222
+ const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(resolveDataDir(), "generated-images");
222
223
  const orchestratorPublicOrigin = (process.env.ORCHESTRATOR_PUBLIC_ORIGIN ?? "http://localhost:4200").replace(/\/+$/, "");
223
224
  args.log?.info({ event: "openai_image_start", model, size, background, outputFormat, promptLength: args.prompt.length }, "Starting OpenAI image generation");
224
225
  const genStartMs = Date.now();
@@ -270,7 +271,7 @@ export async function generateVariationImageWithOpenAI(args) {
270
271
  // Shared image save utility
271
272
  // ---------------------------------------------------------------------------
272
273
  export async function saveGeneratedImage(bytes, prefix = "gen", ext = "png") {
273
- const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(process.cwd(), "../../.data/generated-images");
274
+ const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ?? resolve(resolveDataDir(), "generated-images");
274
275
  const orchestratorPublicOrigin = (process.env.ORCHESTRATOR_PUBLIC_ORIGIN ?? "http://localhost:4200").replace(/\/+$/, "");
275
276
  const fileName = `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
276
277
  await mkdir(generatedImageDir, { recursive: true });
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs";
4
4
  import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { promisify } from "node:util";
6
6
  import { resolve } from "node:path";
7
+ import { resolveDataDir } from "../state/data-dir.js";
7
8
  import { pageDocSchema } from "@avocadostudio-ai/shared";
8
9
  import { draftPages, versions, ensureHeroImageProps, persistStateNow, getSessionPages, getSiteConfig, isLegacySiteId } from "../state/session-state.js";
9
10
  import { toErrorDetail } from "../ops/ops-engine.js";
@@ -413,7 +414,7 @@ export async function publishViaGit(session, content) {
413
414
  let copiedImages = false;
414
415
  if (imageUrlMap.size > 0) {
415
416
  const generatedImageDir = process.env.ORCHESTRATOR_GENERATED_IMAGE_DIR ??
416
- resolve(process.cwd(), "../../.data/generated-images");
417
+ resolve(resolveDataDir(), "generated-images");
417
418
  await mkdir(imageDestDir, { recursive: true });
418
419
  for (const [, fileName] of imageUrlMap) {
419
420
  const src = resolve(generatedImageDir, fileName);
@@ -0,0 +1,6 @@
1
+ export declare function isLegacyMonorepoLayout(cwd?: string): boolean;
2
+ /**
3
+ * The `.data` directory for this process, or the legacy monorepo one when this
4
+ * really is a package inside the monorepo and that directory already exists.
5
+ */
6
+ export declare function resolveDataDir(cwd?: string): string;
@@ -0,0 +1,35 @@
1
+ import { existsSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ /*
4
+ * Where this process keeps its own files: the database, generated images, the
5
+ * agent log.
6
+ *
7
+ * The answer is `<cwd>/.data`. It used to be `<cwd>/../../.data`, which is
8
+ * `apps/orchestrator`'s position in *this* repo and nobody else's — a
9
+ * library-mode host runs from its own project root, so it wrote two directories
10
+ * above itself, outside the project and outside version control, into a
11
+ * directory shared with every sibling checkout that made the same mistake.
12
+ *
13
+ * The legacy location is still honoured for an existing monorepo checkout, but
14
+ * only on proof of workspace membership. The first version of that back-compat
15
+ * check asked `existsSync(legacy)` alone — "has anyone ever created that file",
16
+ * not "am I a package in the workspace that owns it" — so one mistaken write
17
+ * captured every unrelated project under that parent, permanently, because the
18
+ * bug is what created the file that re-triggered it.
19
+ */
20
+ export function isLegacyMonorepoLayout(cwd = process.cwd()) {
21
+ return (existsSync(resolve(cwd, "../../pnpm-workspace.yaml")) &&
22
+ existsSync(resolve(cwd, "package.json")));
23
+ }
24
+ /**
25
+ * The `.data` directory for this process, or the legacy monorepo one when this
26
+ * really is a package inside the monorepo and that directory already exists.
27
+ */
28
+ export function resolveDataDir(cwd = process.cwd()) {
29
+ if (isLegacyMonorepoLayout(cwd)) {
30
+ const legacy = resolve(cwd, "../../.data");
31
+ if (existsSync(legacy))
32
+ return legacy;
33
+ }
34
+ return resolve(cwd, ".data");
35
+ }
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { rename, readFile, readdir, stat, unlink } from "node:fs/promises";
3
3
  import { basename, dirname, resolve } from "node:path";
4
4
  import { SqliteStore } from "./sqlite-store.js";
5
+ import { resolveDataDir } from "./data-dir.js";
5
6
  // ---------------------------------------------------------------------------
6
7
  // Config
7
8
  // ---------------------------------------------------------------------------
@@ -17,23 +18,8 @@ export function resolveDbFile() {
17
18
  return ":memory:";
18
19
  if (process.env.NODE_ENV === "test")
19
20
  return ":memory:";
20
- /*
21
- * `../../.data` is this monorepo's shape, not a general default.
22
- *
23
- * It is right for `apps/orchestrator`, whose cwd is two levels under the repo
24
- * root, and wrong for everyone else: a library-mode host running from its own
25
- * project root writes two directories *above* itself. PBA's landed in
26
- * `~/Projects/.data` — outside the project, outside version control, and
27
- * shared with any sibling checkout that made the same mistake.
28
- *
29
- * The default is now the host's own `.data/`. The legacy path still wins when
30
- * a database is already sitting there, so an existing monorepo checkout keeps
31
- * its state without an env var and without a migration step.
32
- */
33
- const legacy = resolve(process.cwd(), "../../.data/orchestrator.db");
34
- if (existsSync(legacy))
35
- return legacy;
36
- return resolve(process.cwd(), ".data/orchestrator.db");
21
+ // `resolveDataDir` carries the whole story of why this is not `../../.data`.
22
+ return resolve(resolveDataDir(), "orchestrator.db");
37
23
  }
38
24
  export function resolveJsonMigrationTtlDays() {
39
25
  const raw = Number(process.env.ORCHESTRATOR_JSON_MIGRATION_TTL_DAYS ?? 14);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/orchestrator-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./package.json": "./package.json",
@@ -23,8 +23,8 @@
23
23
  "openai": "^4.87.1",
24
24
  "sharp": "^0.34.5",
25
25
  "zod": "^4.3.6",
26
- "@avocadostudio-ai/migration-sdk": "^0.4.0",
27
- "@avocadostudio-ai/shared": "^0.4.0"
26
+ "@avocadostudio-ai/migration-sdk": "^0.5.0",
27
+ "@avocadostudio-ai/shared": "^0.5.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@google/genai": "^1.46.0",