@promptbook/cli 0.112.0-89 → 0.112.0-91

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.
@@ -3,6 +3,7 @@ import {
3
3
  resolveDatabaseMigrationConnectionStringFromEnvironment,
4
4
  runDatabaseMigrations,
5
5
  } from './runDatabaseMigrations';
6
+ import { listEnvironmentRegisteredServers } from '../utils/serverRegistry';
6
7
 
7
8
  /**
8
9
  * Opt-out environment flag for automatic runtime migrations.
@@ -24,7 +25,13 @@ const automaticDatabaseMigrationPromiseByPrefix = new Map<string, Promise<void>>
24
25
  * @private internal startup helper for Agents Server runtime
25
26
  */
26
27
  export async function ensureAutomaticDatabaseMigrations(): Promise<void> {
27
- return ensureAutomaticDatabaseMigrationsForPrefix(process.env.SUPABASE_TABLE_PREFIX || '');
28
+ const environmentServerPrefixes = listEnvironmentRegisteredServers().map((server) => server.tablePrefix);
29
+
30
+ if (environmentServerPrefixes.length === 0) {
31
+ return ensureAutomaticDatabaseMigrationsForPrefix(process.env.SUPABASE_TABLE_PREFIX || '');
32
+ }
33
+
34
+ await Promise.all(environmentServerPrefixes.map((prefix) => ensureAutomaticDatabaseMigrationsForPrefix(prefix)));
28
35
  }
29
36
 
30
37
  /**
@@ -16,6 +16,7 @@ import { listRegisteredServersFromDatabase } from './listRegisteredServersFromDa
16
16
  import { migratePrefix } from './migratePrefix';
17
17
  import { readMigrationFiles, resolveMigrationsDirectory } from './resolveMigrationsDirectory';
18
18
  import { selectPrefixesForMigration } from './selectPrefixesForMigration';
19
+ import { listEnvironmentRegisteredServers } from '../utils/serverRegistry';
19
20
 
20
21
  /**
21
22
  * Allowed values describing who applied a migration record.
@@ -175,7 +176,10 @@ export async function resolveDatabaseMigrationRuntimeConfiguration(
175
176
  try {
176
177
  await client.connect();
177
178
 
178
- const registeredServers = await listRegisteredServersFromDatabase(client);
179
+ const registeredServers = mergeRegisteredServers(
180
+ await listRegisteredServersFromDatabase(client),
181
+ listEnvironmentRegisteredServers(),
182
+ );
179
183
  const hasExplicitDefaultPrefix = process.env[SUPABASE_TABLE_PREFIX_ENV_NAME] !== undefined;
180
184
  const configuredPrefixes = uniquePrefixes([
181
185
  ...(hasExplicitDefaultPrefix
@@ -355,6 +359,32 @@ function uniquePrefixes(prefixes: ReadonlyArray<string>): Array<string> {
355
359
  return result;
356
360
  }
357
361
 
362
+ /**
363
+ * Combines database and environment registry rows, keeping database rows authoritative.
364
+ *
365
+ * @param databaseServers - Persistent `_Server` rows.
366
+ * @param environmentServers - Virtual rows derived from `SERVERS`.
367
+ * @returns Merged rows without duplicate domains.
368
+ */
369
+ function mergeRegisteredServers(
370
+ databaseServers: ReadonlyArray<ServerRecord>,
371
+ environmentServers: ReadonlyArray<ServerRecord>,
372
+ ): Array<ServerRecord> {
373
+ const seenDomains = new Set(databaseServers.map((server) => server.domain));
374
+ const mergedServers = [...databaseServers];
375
+
376
+ for (const environmentServer of environmentServers) {
377
+ if (seenDomains.has(environmentServer.domain)) {
378
+ continue;
379
+ }
380
+
381
+ mergedServers.push(environmentServer);
382
+ seenDomains.add(environmentServer.domain);
383
+ }
384
+
385
+ return mergedServers;
386
+ }
387
+
358
388
  /**
359
389
  * Creates one PostgreSQL client using the lazily loaded `pg` package.
360
390
  *
@@ -750,11 +750,11 @@ class LocalSqliteQueryBuilder implements PromiseLike<LocalSqliteQueryResult> {
750
750
  */
751
751
  private selectMatchingRowids(): Array<number | bigint> {
752
752
  const where = this.createWhereClause();
753
- const sql = `SELECT rowid FROM ${quoteIdentifier(this.tableName)} ${where.sql}`;
753
+ const sql = `SELECT rowid AS "__rowid" FROM ${quoteIdentifier(this.tableName)} ${where.sql}`;
754
754
  return this.database
755
755
  .prepare(sql)
756
756
  .all(...where.values)
757
- .map((row) => row.rowid as number | bigint);
757
+ .map((row) => row.__rowid as number | bigint);
758
758
  }
759
759
 
760
760
  /**
@@ -37,7 +37,7 @@ export async function createMiddlewareRequestContext(request: NextRequest): Prom
37
37
  const requestIp = getRequestIp(request);
38
38
  const host = request.headers.get('host');
39
39
  const supabase = getMiddlewareSupabase();
40
- const registeredServers = supabase ? await loadRegisteredServers() : [];
40
+ const registeredServers = supabase || process.env.SERVERS ? await loadRegisteredServers() : [];
41
41
  const { canQueryServerTables, customDomainResolution, tablePrefixForRequest } = await resolveMiddlewareServerRouting(
42
42
  {
43
43
  host,
@@ -38,6 +38,33 @@ const getCachedProvidedServer = cache(async (): Promise<ProvidedServer> => {
38
38
  const xPromptbookServer = headersList.get('x-promptbook-server');
39
39
 
40
40
  if (isAgentsServerSqliteMode()) {
41
+ if (isLocalDevelopmentHost(requestHost)) {
42
+ return {
43
+ id: null,
44
+ publicUrl: resolveFallbackPublicUrl(requestHost),
45
+ tablePrefix: SUPABASE_TABLE_PREFIX,
46
+ };
47
+ }
48
+
49
+ const registeredServers = await listRegisteredServersUsingServiceRole();
50
+ if (registeredServers.length > 0) {
51
+ const { currentServer: resolvedSqliteServer } = resolveServerSelection({
52
+ host: requestHost,
53
+ forwardedServerHost: xPromptbookServer,
54
+ registeredServers,
55
+ });
56
+
57
+ if (!resolvedSqliteServer) {
58
+ throw new Error(`Server with host "${requestHost}" is not registered in SERVERS`);
59
+ }
60
+
61
+ return {
62
+ id: resolvedSqliteServer.id,
63
+ publicUrl: createServerPublicUrl(resolvedSqliteServer.domain),
64
+ tablePrefix: resolvedSqliteServer.tablePrefix,
65
+ };
66
+ }
67
+
41
68
  return {
42
69
  id: null,
43
70
  publicUrl: resolveFallbackPublicUrl(requestHost),
@@ -62,6 +62,16 @@ const SERVER_REGISTRY_SELECT = 'id,name,environment,domain,tablePrefix,createdAt
62
62
  */
63
63
  const SERVER_REGISTRY_TABLE_NAME = '_Server';
64
64
 
65
+ /**
66
+ * Environment variable with comma-separated standalone server domains.
67
+ */
68
+ const SERVERS_ENV_NAME = 'SERVERS';
69
+
70
+ /**
71
+ * Stable timestamp used by virtual server records derived from environment variables.
72
+ */
73
+ const ENVIRONMENT_SERVER_TIMESTAMP = '1970-01-01T00:00:00.000Z';
74
+
65
75
  /**
66
76
  * In-memory cache TTL for repeated registry lookups inside one runtime process.
67
77
  */
@@ -127,8 +137,10 @@ export async function listRegisteredServers(supabase: Pick<SupabaseClient, 'from
127
137
  export async function listRegisteredServersUsingServiceRole(options?: {
128
138
  readonly forceRefresh?: boolean;
129
139
  }): Promise<Array<ServerRecord>> {
140
+ const environmentServers = listEnvironmentRegisteredServers();
141
+
130
142
  if (isAgentsServerSqliteMode()) {
131
- return [];
143
+ return environmentServers;
132
144
  }
133
145
 
134
146
  const shouldReuseCache =
@@ -137,7 +149,7 @@ export async function listRegisteredServersUsingServiceRole(options?: {
137
149
  Date.now() - cachedServerRegistry.loadedAt < SERVER_REGISTRY_CACHE_TTL_MS;
138
150
 
139
151
  if (shouldReuseCache) {
140
- return cachedServerRegistry!.serversPromise;
152
+ return mergeRegisteredServers(await cachedServerRegistry!.serversPromise, environmentServers);
141
153
  }
142
154
 
143
155
  const serversPromise = listRegisteredServers(getServerRegistryClient());
@@ -147,7 +159,7 @@ export async function listRegisteredServersUsingServiceRole(options?: {
147
159
  };
148
160
 
149
161
  try {
150
- return await serversPromise;
162
+ return mergeRegisteredServers(await serversPromise, environmentServers);
151
163
  } catch (error) {
152
164
  if (cachedServerRegistry?.serversPromise === serversPromise) {
153
165
  cachedServerRegistry = null;
@@ -156,6 +168,35 @@ export async function listRegisteredServersUsingServiceRole(options?: {
156
168
  }
157
169
  }
158
170
 
171
+ /**
172
+ * Loads virtual server records from the comma-separated `SERVERS` environment variable.
173
+ *
174
+ * @returns Server records with deterministic table prefixes derived from normalized domains.
175
+ */
176
+ export function listEnvironmentRegisteredServers(): Array<ServerRecord> {
177
+ const rawServers = process.env[SERVERS_ENV_NAME];
178
+ if (!rawServers) {
179
+ return [];
180
+ }
181
+
182
+ const normalizedDomains = uniqueStrings(
183
+ rawServers
184
+ .split(',')
185
+ .map((server) => normalizeServerDomain(server))
186
+ .filter((server): server is string => Boolean(server)),
187
+ );
188
+
189
+ return normalizedDomains.map((domain, index) => ({
190
+ id: -(index + 1),
191
+ name: domain,
192
+ environment: SERVER_ENVIRONMENT.PRODUCTION,
193
+ domain,
194
+ tablePrefix: buildEnvironmentServerTablePrefix(domain),
195
+ createdAt: ENVIRONMENT_SERVER_TIMESTAMP,
196
+ updatedAt: ENVIRONMENT_SERVER_TIMESTAMP,
197
+ }));
198
+ }
199
+
159
200
  /**
160
201
  * Finds one registered server by incoming host header.
161
202
  *
@@ -404,3 +445,76 @@ function hasHttpProtocol(value: string): boolean {
404
445
  function isDefaultPortForProtocol(protocol: string, port: string): boolean {
405
446
  return (protocol === 'http:' && port === '80') || (protocol === 'https:' && port === '443');
406
447
  }
448
+
449
+ /**
450
+ * Combines database and environment registry rows, keeping database rows authoritative.
451
+ *
452
+ * @param databaseServers - Persistent `_Server` rows.
453
+ * @param environmentServers - Virtual rows derived from `SERVERS`.
454
+ * @returns Merged rows without duplicate domains.
455
+ */
456
+ function mergeRegisteredServers(
457
+ databaseServers: ReadonlyArray<ServerRecord>,
458
+ environmentServers: ReadonlyArray<ServerRecord>,
459
+ ): Array<ServerRecord> {
460
+ const seenDomains = new Set<string>();
461
+ const mergedServers = [...databaseServers];
462
+
463
+ for (const databaseServer of databaseServers) {
464
+ const normalizedDomain = normalizeServerDomain(databaseServer.domain);
465
+ if (normalizedDomain) {
466
+ seenDomains.add(normalizedDomain);
467
+ }
468
+ }
469
+
470
+ for (const environmentServer of environmentServers) {
471
+ const normalizedDomain = normalizeServerDomain(environmentServer.domain);
472
+ if (!normalizedDomain || seenDomains.has(normalizedDomain)) {
473
+ continue;
474
+ }
475
+
476
+ mergedServers.push(environmentServer);
477
+ seenDomains.add(normalizedDomain);
478
+ }
479
+
480
+ return mergedServers;
481
+ }
482
+
483
+ /**
484
+ * Builds a deterministic table prefix from a normalized domain.
485
+ *
486
+ * @param domain - Normalized server domain.
487
+ * @returns Prefix such as `server_www_example_com_`.
488
+ */
489
+ function buildEnvironmentServerTablePrefix(domain: string): string {
490
+ const prefixSuffix = domain
491
+ .toLowerCase()
492
+ .replace(/-/gu, '_dash_')
493
+ .replace(/\./gu, '_')
494
+ .replace(/:/gu, '_port_')
495
+ .replace(/[^a-z0-9_]/gu, '_')
496
+ .replace(/_+/gu, '_')
497
+ .replace(/^_+|_+$/gu, '');
498
+
499
+ return `server_${prefixSuffix}_`;
500
+ }
501
+
502
+ /**
503
+ * Deduplicates non-empty strings while preserving input order.
504
+ *
505
+ * @param values - Raw string values.
506
+ * @returns Unique non-empty strings.
507
+ */
508
+ function uniqueStrings(values: ReadonlyArray<string>): Array<string> {
509
+ const uniqueValues: Array<string> = [];
510
+
511
+ for (const value of values) {
512
+ if (!value || uniqueValues.includes(value)) {
513
+ continue;
514
+ }
515
+
516
+ uniqueValues.push(value);
517
+ }
518
+
519
+ return uniqueValues;
520
+ }
@@ -65,6 +65,12 @@ export declare function listRegisteredServers(supabase: Pick<SupabaseClient, 'fr
65
65
  export declare function listRegisteredServersUsingServiceRole(options?: {
66
66
  readonly forceRefresh?: boolean;
67
67
  }): Promise<Array<ServerRecord>>;
68
+ /**
69
+ * Loads virtual server records from the comma-separated `SERVERS` environment variable.
70
+ *
71
+ * @returns Server records with deterministic table prefixes derived from normalized domains.
72
+ */
73
+ export declare function listEnvironmentRegisteredServers(): Array<ServerRecord>;
68
74
  /**
69
75
  * Finds one registered server by incoming host header.
70
76
  *
package/esm/index.es.js CHANGED
@@ -58,7 +58,7 @@ const BOOK_LANGUAGE_VERSION = '2.0.0';
58
58
  * @generated
59
59
  * @see https://github.com/webgptorg/promptbook
60
60
  */
61
- const PROMPTBOOK_ENGINE_VERSION = '0.112.0-89';
61
+ const PROMPTBOOK_ENGINE_VERSION = '0.112.0-91';
62
62
  /**
63
63
  * TODO: string_promptbook_version should be constrained to the all versions of Promptbook engine
64
64
  * Note: [💞] Ignore a discrepancy between file name and entity name
@@ -36106,6 +36106,12 @@ const PTBK_AGENTS_SERVER_DATABASE_ENV = 'PTBK_AGENTS_SERVER_DATABASE';
36106
36106
  * @private internal constant of `ptbk agents-server`
36107
36107
  */
36108
36108
  const PTBK_AGENTS_SERVER_SQLITE_PATH_ENV = 'PTBK_AGENTS_SERVER_SQLITE_PATH';
36109
+ /**
36110
+ * Optional hostname used by the internal Next server.
36111
+ *
36112
+ * @private internal constant of `ptbk agents-server`
36113
+ */
36114
+ const PTBK_HOSTNAME_ENV = 'PTBK_HOSTNAME';
36109
36115
  /**
36110
36116
  * Entropy size for the local-only token shared by the CLI pump and the Next app.
36111
36117
  *
@@ -36224,8 +36230,15 @@ async function resolveAgentsServerRuntimePaths() {
36224
36230
  * Starts the production Next server and wires its logs into the foreground dashboard.
36225
36231
  */
36226
36232
  function startNextServer(options) {
36233
+ var _a;
36227
36234
  logRunnerEvent(options.logStreams.runner, 'Starting the Agents Server Next process.');
36228
- const commandProcess = spawn(process.execPath, [options.nextCliPath, 'start', '--port', String(options.options.port)], {
36235
+ const nextArguments = [options.nextCliPath, 'start', '--port', String(options.options.port)];
36236
+ const hostname = (_a = options.childEnvironment[PTBK_HOSTNAME_ENV]) === null || _a === void 0 ? void 0 : _a.trim();
36237
+ if (hostname) {
36238
+ nextArguments.push('--hostname', hostname);
36239
+ logRunnerEvent(options.logStreams.runner, `Binding Agents Server Next process to ${hostname}.`);
36240
+ }
36241
+ const commandProcess = spawn(process.execPath, nextArguments, {
36229
36242
  cwd: options.runtimePaths.appPath,
36230
36243
  env: options.childEnvironment,
36231
36244
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -67590,6 +67603,38 @@ const SERVER_ENVIRONMENT = {
67590
67603
  * Global source-of-truth table for server routing and migration targeting.
67591
67604
  */
67592
67605
  const SERVER_REGISTRY_TABLE_NAME = '_Server';
67606
+ /**
67607
+ * Environment variable with comma-separated standalone server domains.
67608
+ */
67609
+ const SERVERS_ENV_NAME = 'SERVERS';
67610
+ /**
67611
+ * Stable timestamp used by virtual server records derived from environment variables.
67612
+ */
67613
+ const ENVIRONMENT_SERVER_TIMESTAMP = '1970-01-01T00:00:00.000Z';
67614
+ /**
67615
+ * Loads virtual server records from the comma-separated `SERVERS` environment variable.
67616
+ *
67617
+ * @returns Server records with deterministic table prefixes derived from normalized domains.
67618
+ */
67619
+ function listEnvironmentRegisteredServers() {
67620
+ const rawServers = process.env[SERVERS_ENV_NAME];
67621
+ if (!rawServers) {
67622
+ return [];
67623
+ }
67624
+ const normalizedDomains = uniqueStrings$1(rawServers
67625
+ .split(',')
67626
+ .map((server) => normalizeServerDomain(server))
67627
+ .filter((server) => Boolean(server)));
67628
+ return normalizedDomains.map((domain, index) => ({
67629
+ id: -(index + 1),
67630
+ name: domain,
67631
+ environment: SERVER_ENVIRONMENT.PRODUCTION,
67632
+ domain,
67633
+ tablePrefix: buildEnvironmentServerTablePrefix(domain),
67634
+ createdAt: ENVIRONMENT_SERVER_TIMESTAMP,
67635
+ updatedAt: ENVIRONMENT_SERVER_TIMESTAMP,
67636
+ }));
67637
+ }
67593
67638
  /**
67594
67639
  * Normalizes one raw `_Server` row and validates required fields.
67595
67640
  *
@@ -67710,6 +67755,39 @@ function hasHttpProtocol(value) {
67710
67755
  function isDefaultPortForProtocol(protocol, port) {
67711
67756
  return (protocol === 'http:' && port === '80') || (protocol === 'https:' && port === '443');
67712
67757
  }
67758
+ /**
67759
+ * Builds a deterministic table prefix from a normalized domain.
67760
+ *
67761
+ * @param domain - Normalized server domain.
67762
+ * @returns Prefix such as `server_www_example_com_`.
67763
+ */
67764
+ function buildEnvironmentServerTablePrefix(domain) {
67765
+ const prefixSuffix = domain
67766
+ .toLowerCase()
67767
+ .replace(/-/gu, '_dash_')
67768
+ .replace(/\./gu, '_')
67769
+ .replace(/:/gu, '_port_')
67770
+ .replace(/[^a-z0-9_]/gu, '_')
67771
+ .replace(/_+/gu, '_')
67772
+ .replace(/^_+|_+$/gu, '');
67773
+ return `server_${prefixSuffix}_`;
67774
+ }
67775
+ /**
67776
+ * Deduplicates non-empty strings while preserving input order.
67777
+ *
67778
+ * @param values - Raw string values.
67779
+ * @returns Unique non-empty strings.
67780
+ */
67781
+ function uniqueStrings$1(values) {
67782
+ const uniqueValues = [];
67783
+ for (const value of values) {
67784
+ if (!value || uniqueValues.includes(value)) {
67785
+ continue;
67786
+ }
67787
+ uniqueValues.push(value);
67788
+ }
67789
+ return uniqueValues;
67790
+ }
67713
67791
 
67714
67792
  /**
67715
67793
  * SQL query used to load all registered servers in a deterministic order.
@@ -68068,7 +68146,7 @@ async function resolveDatabaseMigrationRuntimeConfiguration(logger = console) {
68068
68146
  const client = await createPostgresClient(connectionString);
68069
68147
  try {
68070
68148
  await client.connect();
68071
- const registeredServers = await listRegisteredServersFromDatabase(client);
68149
+ const registeredServers = mergeRegisteredServers(await listRegisteredServersFromDatabase(client), listEnvironmentRegisteredServers());
68072
68150
  const hasExplicitDefaultPrefix = process.env[SUPABASE_TABLE_PREFIX_ENV_NAME] !== undefined;
68073
68151
  const configuredPrefixes = uniquePrefixes([
68074
68152
  ...(hasExplicitDefaultPrefix
@@ -68229,6 +68307,25 @@ function uniquePrefixes(prefixes) {
68229
68307
  }
68230
68308
  return result;
68231
68309
  }
68310
+ /**
68311
+ * Combines database and environment registry rows, keeping database rows authoritative.
68312
+ *
68313
+ * @param databaseServers - Persistent `_Server` rows.
68314
+ * @param environmentServers - Virtual rows derived from `SERVERS`.
68315
+ * @returns Merged rows without duplicate domains.
68316
+ */
68317
+ function mergeRegisteredServers(databaseServers, environmentServers) {
68318
+ const seenDomains = new Set(databaseServers.map((server) => server.domain));
68319
+ const mergedServers = [...databaseServers];
68320
+ for (const environmentServer of environmentServers) {
68321
+ if (seenDomains.has(environmentServer.domain)) {
68322
+ continue;
68323
+ }
68324
+ mergedServers.push(environmentServer);
68325
+ seenDomains.add(environmentServer.domain);
68326
+ }
68327
+ return mergedServers;
68328
+ }
68232
68329
  /**
68233
68330
  * Creates one PostgreSQL client using the lazily loaded `pg` package.
68234
68331
  *