@relyio/cli 0.2.2 → 0.3.1

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/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import http from 'http';
2
3
  import * as fs2 from 'fs';
3
- import { existsSync, readdirSync, statSync, mkdirSync, cpSync } from 'fs';
4
- import path3 from 'path';
4
+ import fs2__default, { existsSync, readFileSync, mkdirSync, readdirSync, writeFileSync, statSync, cpSync } from 'fs';
5
+ import path5 from 'path';
5
6
  import * as os from 'os';
6
7
  import os__default from 'os';
7
8
  import * as yaml from 'yaml';
@@ -9,17 +10,18 @@ import axios, { AxiosError } from 'axios';
9
10
  import axiosRetry from 'axios-retry';
10
11
  import { z } from 'zod';
11
12
  import * as fs4 from 'fs/promises';
12
- import { mkdir, writeFile, rm, readFile, stat } from 'fs/promises';
13
+ import { rm, writeFile, readFile, stat, mkdir } from 'fs/promises';
13
14
  import * as yaml2 from 'js-yaml';
14
15
  import { glob } from 'glob';
15
16
  import { createJiti } from 'jiti';
16
17
  import { fileURLToPath } from 'url';
17
- import { execFileSync, execSync, spawn } from 'child_process';
18
+ import { execFileSync, spawn, execSync } from 'child_process';
19
+ import Handlebars from 'handlebars';
18
20
  import 'object-hash';
19
21
  import 'semver';
20
22
  import { format } from 'prettier';
21
23
  import { structuredPatch, diffWordsWithSpace } from 'diff';
22
- import { randomUUID, createHash } from 'crypto';
24
+ import { randomBytes, randomUUID, createHash } from 'crypto';
23
25
  import React3, { createContext, memo, useState, useEffect, useReducer, useMemo, useCallback, useContext, useRef, useSyncExternalStore } from 'react';
24
26
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
25
27
  import { Box, Text, useApp, Static, useInput, usePaste, useWindowSize } from 'ink';
@@ -35,13 +37,11 @@ import figures from 'figures';
35
37
  import { table as table$1 } from 'table';
36
38
  import readline from 'readline';
37
39
  import open from 'open';
38
- import http from 'http';
39
- import inquirer from 'inquirer';
40
+ import inquirer2 from 'inquirer';
40
41
  import { Listr } from 'listr2';
41
42
  import 'strip-ansi';
42
43
  import { watch } from 'chokidar';
43
44
  import { EventEmitter } from 'events';
44
- import Handlebars from 'handlebars';
45
45
 
46
46
  var __defProp = Object.defineProperty;
47
47
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -52,6 +52,143 @@ var __export = (target, all) => {
52
52
  for (var name in all)
53
53
  __defProp(target, name, { get: all[name], enumerable: true });
54
54
  };
55
+ async function startCallbackServer(options = {}) {
56
+ const host = options.host ?? "127.0.0.1";
57
+ const callbackPath = options.callbackPath ?? "/callback";
58
+ const server = http.createServer();
59
+ await new Promise((resolve, reject) => {
60
+ server.once("error", reject);
61
+ server.listen(0, host, resolve);
62
+ });
63
+ const { port } = server.address();
64
+ const origin = `http://${host}:${port}`;
65
+ const redirectUri = `${origin}${callbackPath === "/" ? "" : callbackPath}`;
66
+ let settled = false;
67
+ let waiter;
68
+ const held = [];
69
+ const close = () => {
70
+ try {
71
+ server.close();
72
+ server.closeAllConnections?.();
73
+ } catch {
74
+ }
75
+ };
76
+ const decide = (query, res, active) => {
77
+ if (query.get("state") !== active.expectedState) {
78
+ settled = true;
79
+ html(res, 400, errorHtml("State mismatch"));
80
+ close();
81
+ active.settle({
82
+ ok: false,
83
+ error: new Error("Sign-in state mismatch \u2014 aborting for safety.")
84
+ });
85
+ return;
86
+ }
87
+ const error2 = query.get("error");
88
+ if (error2 !== null) {
89
+ settled = true;
90
+ html(res, 400, errorHtml(error2));
91
+ close();
92
+ active.settle({ ok: false, error: new Error(`Authorization failed: ${error2}`) });
93
+ return;
94
+ }
95
+ settled = true;
96
+ html(res, 200, SUCCESS_HTML);
97
+ close();
98
+ active.settle({ ok: true, query });
99
+ };
100
+ server.on("request", (req, res) => {
101
+ const url = new URL(req.url ?? "/", origin);
102
+ if (url.pathname !== callbackPath) {
103
+ res.writeHead(404).end();
104
+ return;
105
+ }
106
+ if (settled) {
107
+ html(res, 200, SUCCESS_HTML);
108
+ return;
109
+ }
110
+ if (url.search === "") {
111
+ html(res, 200, WAITING_HTML);
112
+ return;
113
+ }
114
+ if (waiter === void 0) {
115
+ held.push({ query: url.searchParams, res });
116
+ return;
117
+ }
118
+ decide(url.searchParams, res, waiter);
119
+ });
120
+ const waitForCallback = (expectedState, timeoutMs = DEFAULT_CALLBACK_TIMEOUT_MS) => new Promise((resolve, reject) => {
121
+ const timer = setTimeout(() => {
122
+ if (settled) return;
123
+ settled = true;
124
+ close();
125
+ reject(new Error("Timed out waiting for the browser sign-in to complete."));
126
+ }, timeoutMs);
127
+ timer.unref?.();
128
+ const active = {
129
+ expectedState,
130
+ settle: (outcome) => {
131
+ clearTimeout(timer);
132
+ if (outcome.ok) resolve(outcome.query);
133
+ else reject(outcome.error);
134
+ }
135
+ };
136
+ waiter = active;
137
+ const [first, ...rest] = held.splice(0);
138
+ for (const extra of rest) html(extra.res, 200, SUCCESS_HTML);
139
+ if (first) decide(first.query, first.res, active);
140
+ });
141
+ const waitForCode = async (expectedState, timeoutMs) => {
142
+ const query = await waitForCallback(expectedState, timeoutMs);
143
+ const code = query.get("code");
144
+ if (code === null || code === "") {
145
+ throw new Error("The sign-in callback carried no authorization code.");
146
+ }
147
+ return code;
148
+ };
149
+ return { redirectUri, waitForCallback, waitForCode, close };
150
+ }
151
+ function escapeHtml(value) {
152
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
153
+ }
154
+ function writeSecretFile(filePath, data) {
155
+ fs2__default.mkdirSync(path5.dirname(filePath), { recursive: true });
156
+ fs2__default.writeFileSync(filePath, JSON.stringify(data, null, 2), {
157
+ encoding: "utf8",
158
+ mode: SECRET_FILE_MODE
159
+ });
160
+ fs2__default.chmodSync(filePath, SECRET_FILE_MODE);
161
+ }
162
+ function readSecretFile(filePath) {
163
+ try {
164
+ if (!fs2__default.existsSync(filePath)) return null;
165
+ return JSON.parse(fs2__default.readFileSync(filePath, "utf8"));
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ var DEFAULT_CALLBACK_TIMEOUT_MS, SUCCESS_HTML, WAITING_HTML, errorHtml, html, SECRET_FILE_MODE;
171
+ var init_dist = __esm({
172
+ "../device-auth/dist/index.js"() {
173
+ DEFAULT_CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
174
+ SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>RelyIO</title>
175
+ <style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;text-align:center;padding:3rem;color:#111}h1{color:#16a34a}</style>
176
+ </head><body><h1>Connected to RelyIO</h1><p>Authentication succeeded. You can close this tab and return to your terminal.</p></body></html>`;
177
+ WAITING_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>RelyIO</title>
178
+ <style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;text-align:center;padding:3rem;color:#111}</style>
179
+ </head><body><h1>Waiting for RelyIO</h1><p>Finish signing in in the tab that opened, then come back here.</p></body></html>`;
180
+ errorHtml = (message) => `<!doctype html><html><head><meta charset="utf-8"><title>RelyIO</title>
181
+ <style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;text-align:center;padding:3rem;color:#111}h1{color:#dc2626}</style>
182
+ </head><body><h1>Authentication failed</h1><p>${escapeHtml(message)}</p></body></html>`;
183
+ html = (res, status, body) => {
184
+ res.writeHead(status, {
185
+ "content-type": "text/html; charset=utf-8",
186
+ "cache-control": "no-store"
187
+ }).end(body);
188
+ };
189
+ SECRET_FILE_MODE = 384;
190
+ }
191
+ });
55
192
 
56
193
  // src/config/secret-store.ts
57
194
  var secret_store_exports = {};
@@ -91,19 +228,10 @@ function emitPlaintextWarning() {
91
228
  );
92
229
  }
93
230
  function readLegacyFile() {
94
- try {
95
- if (!fs2.existsSync(LEGACY_CONFIG_FILE)) return {};
96
- const raw = fs2.readFileSync(LEGACY_CONFIG_FILE, "utf8");
97
- return JSON.parse(raw);
98
- } catch {
99
- return {};
100
- }
231
+ return readSecretFile(LEGACY_CONFIG_FILE) ?? {};
101
232
  }
102
233
  function writeLegacyFile(data) {
103
- if (!fs2.existsSync(LEGACY_CONFIG_DIR)) {
104
- fs2.mkdirSync(LEGACY_CONFIG_DIR, { recursive: true });
105
- }
106
- fs2.writeFileSync(LEGACY_CONFIG_FILE, JSON.stringify(data, null, 2), "utf8");
234
+ writeSecretFile(LEGACY_CONFIG_FILE, data);
107
235
  }
108
236
  async function setSecret(key, value) {
109
237
  cache.set(key, value);
@@ -183,9 +311,10 @@ function __resetForTests() {
183
311
  var SERVICE_NAME, LEGACY_CONFIG_DIR, LEGACY_CONFIG_FILE, keytarPromise, keychainAvailable, plaintextWarningShown, cache;
184
312
  var init_secret_store = __esm({
185
313
  "src/config/secret-store.ts"() {
314
+ init_dist();
186
315
  SERVICE_NAME = "relyio-cli";
187
- LEGACY_CONFIG_DIR = path3.join(os.homedir(), ".relyio");
188
- LEGACY_CONFIG_FILE = path3.join(LEGACY_CONFIG_DIR, "config.json");
316
+ LEGACY_CONFIG_DIR = path5.join(os.homedir(), ".relyio");
317
+ LEGACY_CONFIG_FILE = path5.join(LEGACY_CONFIG_DIR, "config.json");
189
318
  plaintextWarningShown = false;
190
319
  cache = /* @__PURE__ */ new Map();
191
320
  }
@@ -233,28 +362,19 @@ function ensureDirectory(dir) {
233
362
  }
234
363
  }
235
364
  function readGlobalConfig() {
236
- try {
237
- ensureDirectory(GLOBAL_CONFIG_DIR);
238
- if (fs2.existsSync(GLOBAL_CONFIG_FILE)) {
239
- const configData = fs2.readFileSync(GLOBAL_CONFIG_FILE, "utf8");
240
- return JSON.parse(configData);
241
- }
242
- } catch (error2) {
243
- debugError("Error reading global config", error2);
244
- }
245
- return { ...defaultGlobalConfig };
365
+ const stored = readSecretFile(GLOBAL_CONFIG_FILE);
366
+ return stored ?? { ...defaultGlobalConfig };
246
367
  }
247
368
  function writeGlobalConfig(config) {
248
369
  try {
249
- ensureDirectory(GLOBAL_CONFIG_DIR);
250
- fs2.writeFileSync(GLOBAL_CONFIG_FILE, JSON.stringify(config, null, 2), "utf8");
370
+ writeSecretFile(GLOBAL_CONFIG_FILE, config);
251
371
  } catch (error2) {
252
372
  debugError("Error writing global config", error2);
253
373
  throw new Error("Failed to write global configuration");
254
374
  }
255
375
  }
256
376
  function getProjectConfigPath() {
257
- return path3.join(process.cwd(), PROJECT_CONFIG_FILE);
377
+ return path5.join(process.cwd(), PROJECT_CONFIG_FILE);
258
378
  }
259
379
  function readProjectConfig() {
260
380
  try {
@@ -278,7 +398,7 @@ function writeProjectConfig(config) {
278
398
  }
279
399
  }
280
400
  function getLocalConfigPath() {
281
- return path3.join(process.cwd(), LOCAL_CONFIG_FILE);
401
+ return path5.join(process.cwd(), LOCAL_CONFIG_FILE);
282
402
  }
283
403
  function readLocalConfig() {
284
404
  try {
@@ -294,7 +414,7 @@ function readLocalConfig() {
294
414
  }
295
415
  function writeLocalConfig(config) {
296
416
  try {
297
- const configDir = path3.join(process.cwd(), LOCAL_CONFIG_DIR);
417
+ const configDir = path5.join(process.cwd(), LOCAL_CONFIG_DIR);
298
418
  ensureDirectory(configDir);
299
419
  const configPath = getLocalConfigPath();
300
420
  fs2.writeFileSync(configPath, yaml.stringify(config), "utf8");
@@ -428,13 +548,14 @@ async function isAuthenticated() {
428
548
  var isDebug, GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE, PROJECT_CONFIG_FILE, LOCAL_CONFIG_DIR, LOCAL_CONFIG_FILE, PROD_API_URL, PROD_DASHBOARD_URL, defaultGlobalConfig, defaultProjectConfig, defaultLocalConfig, getAuthTokenAsync, getApiKeyAsync, config_manager_default;
429
549
  var init_config_manager = __esm({
430
550
  "src/config/config-manager.ts"() {
551
+ init_dist();
431
552
  init_secret_store();
432
553
  isDebug = () => process.env.DEBUG === "true" || process.env.DEBUG === "1";
433
- GLOBAL_CONFIG_DIR = path3.join(os.homedir(), ".relyio");
434
- GLOBAL_CONFIG_FILE = path3.join(GLOBAL_CONFIG_DIR, "config.json");
554
+ GLOBAL_CONFIG_DIR = path5.join(os.homedir(), ".relyio");
555
+ GLOBAL_CONFIG_FILE = path5.join(GLOBAL_CONFIG_DIR, "config.json");
435
556
  PROJECT_CONFIG_FILE = ".relyio-context.json";
436
557
  LOCAL_CONFIG_DIR = "relyio";
437
- LOCAL_CONFIG_FILE = path3.join(LOCAL_CONFIG_DIR, "config.yaml");
558
+ LOCAL_CONFIG_FILE = path5.join(LOCAL_CONFIG_DIR, "config.yaml");
438
559
  PROD_API_URL = "https://api.relyio.com";
439
560
  PROD_DASHBOARD_URL = "https://dashboard.relyio.com";
440
561
  defaultGlobalConfig = {};
@@ -542,12 +663,32 @@ var init_config = __esm({
542
663
  config_default = config_manager_default;
543
664
  }
544
665
  });
545
- var DEFAULT_TIMEOUT_MS, isDebug2, ApiClient, apiClient, client_default;
666
+ function redactForLog(data) {
667
+ if (typeof data === "string") {
668
+ try {
669
+ return redactForLog(JSON.parse(data));
670
+ } catch {
671
+ return data;
672
+ }
673
+ }
674
+ if (Array.isArray(data)) return data.map((item) => redactForLog(item));
675
+ if (typeof data === "object" && data !== null) {
676
+ return Object.fromEntries(
677
+ Object.entries(data).map(([key, value]) => [
678
+ key,
679
+ SECRET_FIELD.test(key) ? "***" : redactForLog(value)
680
+ ])
681
+ );
682
+ }
683
+ return data;
684
+ }
685
+ var DEFAULT_TIMEOUT_MS, isDebug2, SECRET_FIELD, ApiClient, apiClient, client_default;
546
686
  var init_client = __esm({
547
687
  "src/api/client.ts"() {
548
688
  init_config();
549
689
  DEFAULT_TIMEOUT_MS = 3e4;
550
690
  isDebug2 = () => process.env.DEBUG === "true" || process.env.DEBUG === "1";
691
+ SECRET_FIELD = /secret|token|password/i;
551
692
  ApiClient = class {
552
693
  client;
553
694
  constructor() {
@@ -635,9 +776,8 @@ var init_client = __esm({
635
776
  }
636
777
  }
637
778
  if (reqConfig.data) {
638
- const data = typeof reqConfig.data === "string" ? reqConfig.data : JSON.stringify(reqConfig.data);
639
779
  curlCommand += ` \\
640
- -d '${data}'`;
780
+ -d '${JSON.stringify(redactForLog(reqConfig.data))}'`;
641
781
  }
642
782
  console.error(`[relyio-cli][debug] ${curlCommand}`);
643
783
  }
@@ -682,21 +822,21 @@ var init_client = __esm({
682
822
  }
683
823
  }
684
824
  // Helper methods for common HTTP methods
685
- async get(path23, options) {
825
+ async get(path25, options) {
686
826
  return this.request({
687
827
  method: "GET",
688
- url: path23,
828
+ url: path25,
689
829
  params: options?.params,
690
830
  headers: options?.headers
691
831
  });
692
832
  }
693
- async post(path23, data, headers) {
694
- return this.request({ method: "POST", url: path23, data, headers });
833
+ async post(path25, data, headers) {
834
+ return this.request({ method: "POST", url: path25, data, headers });
695
835
  }
696
- async put(path23, data, headers) {
697
- return this.request({ method: "PUT", url: path23, data, headers });
836
+ async put(path25, data, headers) {
837
+ return this.request({ method: "PUT", url: path25, data, headers });
698
838
  }
699
- async delete(path23, headers) {
839
+ async delete(path25, headers) {
700
840
  const customHeaders = {
701
841
  ...headers,
702
842
  // Remove Content-Type for DELETE requests as they typically don't have a body
@@ -704,7 +844,7 @@ var init_client = __esm({
704
844
  };
705
845
  return this.request({
706
846
  method: "DELETE",
707
- url: path23,
847
+ url: path25,
708
848
  headers: customHeaders
709
849
  });
710
850
  }
@@ -714,38 +854,10 @@ var init_client = __esm({
714
854
  }
715
855
  });
716
856
 
717
- // src/api/schema.ts
718
- async function getSchemaStatus(projectId, branchId, localSchema) {
719
- const response = await client_default.post(
720
- `/projects/${projectId}/branches/${branchId}/schema/status`,
721
- localSchema || {}
722
- );
723
- return response.data;
724
- }
725
- async function deploySchema(projectId, branchId, localSchema, options) {
726
- const response = await client_default.post(
727
- `/projects/${projectId}/branches/${branchId}/schema/deploy`,
728
- {
729
- localSchema,
730
- ...options
731
- }
732
- );
733
- const data = response.data;
734
- if ("data" in data && typeof data.data === "object") {
735
- return data.data;
736
- }
737
- return data;
738
- }
739
- var init_schema = __esm({
740
- "src/api/schema.ts"() {
741
- init_client();
742
- }
743
- });
744
-
745
- // ../../platform/api-contract/dist/chunk-QKXHDVSP.js
857
+ // ../../platform/api-contract/dist/chunk-MFO7NR2F.js
746
858
  var ERROR_CODES;
747
- var init_chunk_QKXHDVSP = __esm({
748
- "../../platform/api-contract/dist/chunk-QKXHDVSP.js"() {
859
+ var init_chunk_MFO7NR2F = __esm({
860
+ "../../platform/api-contract/dist/chunk-MFO7NR2F.js"() {
749
861
  ERROR_CODES = {
750
862
  MISSING_API_KEY: { status: 401 },
751
863
  INVALID_API_KEY: { status: 401 },
@@ -757,7 +869,7 @@ var init_chunk_QKXHDVSP = __esm({
757
869
  REQUEST_INVALID: { status: 400 },
758
870
  CONTENT_INVALID: { status: 400 },
759
871
  /**
760
- * Added in API3a, and the only name added to §4's approved seventeen.
872
+ * Added in API3a, the first name added to §4's approved seventeen.
761
873
  *
762
874
  * `apps/edge/cdn` answers any method other than `GET`, `HEAD` or `OPTIONS`
763
875
  * with `405`, and no code in §4 carries that status. The two alternatives
@@ -775,6 +887,37 @@ var init_chunk_QKXHDVSP = __esm({
775
887
  * approves those. Approved 2026-09-06 (plan §4, decision record in §16 API3a).
776
888
  */
777
889
  METHOD_NOT_ALLOWED: { status: 405 },
890
+ /**
891
+ * Added by REL-657, the second name added to §4's approved seventeen.
892
+ *
893
+ * A session that verified a factor too long ago may not hand authority out:
894
+ * mint an Access Token, approve an app, delete the account. `ACCESS_DENIED`
895
+ * is what a subject may never do; this is what a subject may do once they
896
+ * have proved the session is still theirs, and a client does something
897
+ * different on reading it — it raises a Reverification and retries — which
898
+ * is Decision 3's test. `details.level` names the proof the client asks for.
899
+ *
900
+ * A **customer-facing naming decision**, approved by the product owner on
901
+ * 2026-09-20 (REL-657, spec 016).
902
+ */
903
+ REVERIFICATION_REQUIRED: { status: 403 },
904
+ /**
905
+ * Added by REL-1080, the third name added to §4's approved seventeen.
906
+ *
907
+ * A downgrade (or cancel) that would leave the Organization keeping more than
908
+ * the target Plan allows — more Projects, Members or stored bytes than its
909
+ * Stock Limits — is refused, because the lower Plan would otherwise keep
910
+ * serving what it refuses to admit. It is not `ACCESS_DENIED` (the subject may
911
+ * downgrade) and not `RESOURCE_CONFLICT` (no version raced): a client reading
912
+ * it does something no other code tells it — shows `details.stockLimits`, the
913
+ * same list the downgrade preview reads, and asks the customer to reduce what
914
+ * they keep before retrying. 409 because the request conflicts with the
915
+ * Organization's current state and succeeds unchanged once that state does.
916
+ *
917
+ * A **customer-facing naming decision**, approved by the product owner on
918
+ * 2026-09-24 (REL-1080).
919
+ */
920
+ PLAN_DOWNGRADE_BLOCKED: { status: 409 },
778
921
  RESOURCE_NOT_FOUND: { status: 404 },
779
922
  RESOURCE_CONFLICT: { status: 409 },
780
923
  RESOURCE_EXPIRED: { status: 410 },
@@ -821,7 +964,7 @@ function readError(body) {
821
964
  var ContractBodyError;
822
965
  var init_client2 = __esm({
823
966
  "../../platform/api-contract/dist/client/index.js"() {
824
- init_chunk_QKXHDVSP();
967
+ init_chunk_MFO7NR2F();
825
968
  ContractBodyError = class extends Error {
826
969
  body;
827
970
  constructor(message, body) {
@@ -868,9 +1011,11 @@ async function getProject(projectId) {
868
1011
  }
869
1012
  async function getProjectBranches(projectId, filter) {
870
1013
  const params = new URLSearchParams();
871
- if (filter?.name) params.append("name", filter.name);
872
- if (filter?.sortField) params.append("sortField", filter.sortField);
873
- if (filter?.sortDirection) params.append("sortDirection", filter.sortDirection);
1014
+ if (filter?.name) params.append("name", `eq:${filter.name}`);
1015
+ if (filter?.sortField) {
1016
+ params.append("sort", filter.sortField);
1017
+ if (filter.sortDirection) params.append("order", filter.sortDirection);
1018
+ }
874
1019
  params.append("limit", "100");
875
1020
  const queryString = params.toString();
876
1021
  const url = `/projects/${projectId}/branches${queryString ? `?${queryString}` : ""}`;
@@ -916,6 +1061,26 @@ async function compareBranchSchemas(projectId, sourceBranchId, targetBranchId) {
916
1061
  )
917
1062
  );
918
1063
  }
1064
+ async function createProjectApiKey(projectId, body) {
1065
+ return readPayload(
1066
+ await client_default.post(`/projects/${projectId}/api-keys`, body)
1067
+ );
1068
+ }
1069
+ async function getProjectConnections(projectId) {
1070
+ return readPayload(
1071
+ await client_default.get(`/projects/${projectId}/connections`)
1072
+ );
1073
+ }
1074
+ async function createConnection(projectId, body) {
1075
+ return readPayload(
1076
+ await client_default.post(`/projects/${projectId}/connections`, body)
1077
+ );
1078
+ }
1079
+ async function createAutomation(projectId, body) {
1080
+ return readPayload(
1081
+ await client_default.post(`/projects/${projectId}/automations`, body)
1082
+ );
1083
+ }
919
1084
  var manage_default;
920
1085
  var init_manage = __esm({
921
1086
  "src/api/manage.ts"() {
@@ -932,11 +1097,43 @@ var init_manage = __esm({
932
1097
  createBranchWithParentDetection,
933
1098
  deleteBranch,
934
1099
  getBranchDivergence,
935
- compareBranchSchemas
1100
+ compareBranchSchemas,
1101
+ createProjectApiKey,
1102
+ getProjectConnections,
1103
+ createConnection,
1104
+ createAutomation
936
1105
  };
937
1106
  }
938
1107
  });
939
1108
 
1109
+ // src/api/schema.ts
1110
+ async function getSchemaStatus(projectId, branchId, localSchema) {
1111
+ const response = await client_default.post(
1112
+ `/projects/${projectId}/branches/${branchId}/schema/status`,
1113
+ localSchema || {}
1114
+ );
1115
+ return response.data;
1116
+ }
1117
+ async function deploySchema(projectId, branchId, localSchema, options) {
1118
+ const response = await client_default.post(
1119
+ `/projects/${projectId}/branches/${branchId}/schema/deploy`,
1120
+ {
1121
+ localSchema,
1122
+ ...options
1123
+ }
1124
+ );
1125
+ const data = response.data;
1126
+ if ("data" in data && typeof data.data === "object") {
1127
+ return data.data;
1128
+ }
1129
+ return data;
1130
+ }
1131
+ var init_schema = __esm({
1132
+ "src/api/schema.ts"() {
1133
+ init_client();
1134
+ }
1135
+ });
1136
+
940
1137
  // src/api/manage-error.ts
941
1138
  function manageErrorBody(error2) {
942
1139
  const candidate = error2;
@@ -977,24 +1174,24 @@ var init_manage_error = __esm({
977
1174
  });
978
1175
  function detectCircularFallback(startCode, locales) {
979
1176
  const visited = /* @__PURE__ */ new Set();
980
- const path23 = [];
1177
+ const path25 = [];
981
1178
  const localeMap = new Map(locales.map((l) => [l.code, l]));
982
1179
  function visit(code) {
983
1180
  if (visited.has(code)) {
984
- const cycleStart = path23.indexOf(code);
985
- return [...path23.slice(cycleStart), code];
1181
+ const cycleStart = path25.indexOf(code);
1182
+ return [...path25.slice(cycleStart), code];
986
1183
  }
987
1184
  const locale = localeMap.get(code);
988
1185
  if (!locale || !locale.fallback || locale.fallback.length === 0) {
989
1186
  return null;
990
1187
  }
991
1188
  visited.add(code);
992
- path23.push(code);
1189
+ path25.push(code);
993
1190
  for (const fallbackCode of locale.fallback) {
994
1191
  const cycle = visit(fallbackCode);
995
1192
  if (cycle) return cycle;
996
1193
  }
997
- path23.pop();
1194
+ path25.pop();
998
1195
  visited.delete(code);
999
1196
  return null;
1000
1197
  }
@@ -1086,6 +1283,14 @@ var init_chunk_VGBOPF6G = __esm({
1086
1283
  });
1087
1284
  }
1088
1285
  });
1286
+
1287
+ // ../../domain/schema/dist/chunk-JH6U6ENN.js
1288
+ var TIME_UNITS;
1289
+ var init_chunk_JH6U6ENN = __esm({
1290
+ "../../domain/schema/dist/chunk-JH6U6ENN.js"() {
1291
+ TIME_UNITS = ["minutes", "hours", "days", "weeks", "months", "years"];
1292
+ }
1293
+ });
1089
1294
  function sortDependencyLevels(nodes, dependencies) {
1090
1295
  const nodeSet = new Set(nodes);
1091
1296
  const dependents = /* @__PURE__ */ new Map();
@@ -1127,21 +1332,21 @@ function sortDependencyLevels(nodes, dependencies) {
1127
1332
  function extractCycle(leftover, dependencies) {
1128
1333
  const leftoverSet = new Set(leftover);
1129
1334
  const visiting = /* @__PURE__ */ new Set();
1130
- const path23 = [];
1335
+ const path25 = [];
1131
1336
  const dfs = (node) => {
1132
1337
  if (visiting.has(node)) {
1133
- const start = path23.indexOf(node);
1134
- return [...path23.slice(start), node];
1338
+ const start = path25.indexOf(node);
1339
+ return [...path25.slice(start), node];
1135
1340
  }
1136
1341
  if (!leftoverSet.has(node)) return null;
1137
1342
  visiting.add(node);
1138
- path23.push(node);
1343
+ path25.push(node);
1139
1344
  for (const dep of dependencies.get(node) ?? []) {
1140
1345
  if (!leftoverSet.has(dep)) continue;
1141
1346
  const cycle = dfs(dep);
1142
1347
  if (cycle) return cycle;
1143
1348
  }
1144
- path23.pop();
1349
+ path25.pop();
1145
1350
  visiting.delete(node);
1146
1351
  leftoverSet.delete(node);
1147
1352
  return null;
@@ -1171,7 +1376,7 @@ function fieldsFromDirectOrFieldsets(type) {
1171
1376
  function refineDeclaredReadSets(fields, ctx) {
1172
1377
  const knownIds = new Set(fields.map((entry) => entry.field.id));
1173
1378
  const dependencies = /* @__PURE__ */ new Map();
1174
- for (const { field, path: path23 } of fields) {
1379
+ for (const { field, path: path25 } of fields) {
1175
1380
  const inputs = field.ai?.inputs;
1176
1381
  if (!inputs || inputs.length === 0) continue;
1177
1382
  const valid2 = [];
@@ -1180,7 +1385,7 @@ function refineDeclaredReadSets(fields, ctx) {
1180
1385
  ctx.addIssue({
1181
1386
  code: z.ZodIssueCode.custom,
1182
1387
  message: UNKNOWN_INPUT_MESSAGE(field.id, inputId),
1183
- path: [...path23, "ai", "inputs"]
1388
+ path: [...path25, "ai", "inputs"]
1184
1389
  });
1185
1390
  continue;
1186
1391
  }
@@ -1200,9 +1405,10 @@ function refineDeclaredReadSets(fields, ctx) {
1200
1405
  });
1201
1406
  }
1202
1407
  }
1203
- var AI_INSTRUCTIONS_MAX_CHARS, AIConfigSchema, FieldAIConfigSchema, AssetAIConfigSchema, BaseUIConfigSchema, TextUIOptionsSchema, UIConfigSchema, RuleConditionSchema, RuleConditionsSchema, VisibilityRuleSchema, RequirementRuleSchema, ValidationRuleSchema, ReadOnlyRuleSchema, FieldRuleSchema, LocalizationConfigSchema, FieldSchema, FieldsetSchema, BUILT_IN_ROLES, ROLE_ACTIONS, RoleActionSchema, BuiltInRoleSchema, roleIdPattern, RoleIdSchema, accessTagPattern, AccessTagSchema, AccessTagsArraySchema, ResourcePermissionSchema, FieldVisibilitySchema, RolePermissionsSchema, RoleSchema, RolesArraySchema, stageIdPattern, WorkflowEscalationSchema, WorkflowAutoAssignSchema, WorkflowStageSchema, WorkflowConfigSchema, ContentTypeWorkflowSchema, FILTER_OPERATORS, FilterOperatorSchema, TEXT_OPERATORS, LONG_TEXT_OPERATORS, NUMERIC_OPERATORS, BOOLEAN_OPERATORS, ENUM_OPERATORS, DATE_OPERATORS, REFERENCE_OPERATORS, TAGS_OPERATORS, FIELD_TYPE_OPERATORS, TIME_UNITS, TimeUnitSchema, RelativeDateValueSchema, COLUMN_RENDERERS, ColumnRendererSchema, ASSET_KINDS, FORBIDDEN_ASSET_TYPE_FIELD_TYPES, IMAGE_OUTPUT_FORMATS, IMAGE_FIT_MODES, UNKNOWN_INPUT_MESSAGE, INPUT_CYCLE_MESSAGE, FitSchema, OutputFormatSchema, ImageTransformSchema, ImageUploadConfigSchema, VideoUploadConfigSchema, FileUploadConfigSchema, ImageExifConfigSchema, VariantDefinitionSchema, ImageSourceConfigSchema, ImageKindConfigSchema, VideoPosterConfigSchema, VideoCaptionsConfigSchema, VideoDownloadConfigSchema, VideoWatermarkConfigSchema, VideoKindConfigSchema, FileScanConfigSchema, FileKindConfigSchema, AssetTypeSchema;
1204
- var init_chunk_NB2NTUXG = __esm({
1205
- "../../domain/schema/dist/chunk-NB2NTUXG.js"() {
1408
+ var AI_INSTRUCTIONS_MAX_CHARS, AIConfigSchema, FieldAIConfigSchema, AssetAIConfigSchema, BaseUIConfigSchema, TextUIOptionsSchema, UIConfigSchema, RuleConditionSchema, RuleConditionsSchema, VisibilityRuleSchema, RequirementRuleSchema, ValidationRuleSchema, ReadOnlyRuleSchema, FieldRuleSchema, LocalizationConfigSchema, FieldSchema, FieldsetSchema, BUILT_IN_ROLES, ROLE_ACTIONS, RoleActionSchema, BuiltInRoleSchema, roleIdPattern, RoleIdSchema, accessTagPattern, AccessTagSchema, AccessTagsArraySchema, ResourcePermissionSchema, FieldVisibilitySchema, RolePermissionsSchema, RoleSchema, RolesArraySchema, stageIdPattern, WorkflowEscalationSchema, WorkflowAutoAssignSchema, WorkflowStageSchema, WorkflowConfigSchema, ContentTypeWorkflowSchema, FILTER_OPERATORS, FilterOperatorSchema, TEXT_OPERATORS, LONG_TEXT_OPERATORS, NUMERIC_OPERATORS, BOOLEAN_OPERATORS, ENUM_OPERATORS, DATE_OPERATORS, REFERENCE_OPERATORS, TAGS_OPERATORS, FIELD_TYPE_OPERATORS, TimeUnitSchema, RelativeDateValueSchema, COLUMN_RENDERERS, ColumnRendererSchema, ASSET_KINDS, FORBIDDEN_ASSET_TYPE_FIELD_TYPES, IMAGE_OUTPUT_FORMATS, IMAGE_FIT_MODES, UNKNOWN_INPUT_MESSAGE, INPUT_CYCLE_MESSAGE, FitSchema, OutputFormatSchema, ImageTransformSchema, ImageUploadConfigSchema, VideoUploadConfigSchema, FileUploadConfigSchema, ImageExifConfigSchema, VariantDefinitionSchema, ImageSourceConfigSchema, ImageKindConfigSchema, VideoPosterConfigSchema, VideoCaptionsConfigSchema, VideoDownloadConfigSchema, VideoWatermarkConfigSchema, VideoKindConfigSchema, FileScanConfigSchema, FileKindConfigSchema, AssetTypeSchema;
1409
+ var init_chunk_PCG6XHFD = __esm({
1410
+ "../../domain/schema/dist/chunk-PCG6XHFD.js"() {
1411
+ init_chunk_JH6U6ENN();
1206
1412
  AI_INSTRUCTIONS_MAX_CHARS = 1e3;
1207
1413
  AIConfigSchema = z.object({
1208
1414
  /**
@@ -1669,7 +1875,6 @@ var init_chunk_NB2NTUXG = __esm({
1669
1875
  list: []
1670
1876
  // Lists are complex, use nested filters
1671
1877
  };
1672
- TIME_UNITS = ["minutes", "hours", "days", "weeks", "months", "years"];
1673
1878
  TimeUnitSchema = z.enum(TIME_UNITS);
1674
1879
  RelativeDateValueSchema = z.object({
1675
1880
  amount: z.number().int().positive(),
@@ -2057,11 +2262,42 @@ var init_chunk_NB2NTUXG = __esm({
2057
2262
  });
2058
2263
  }
2059
2264
  });
2060
- var StringOperationSchema, StringOperationWithParamsSchema, WhereValueSchema, WhereConditionSchema, SortConditionSchema, EnhancedIncludeObjectSchema, IncludeDefinitionSchema, SelectMappingSchema, FilterDefinitionSchema, FullTextSearchSchema, ContentTypesSchema, SearchBlockSchema, SimilarBlockSchema, AggregationMetricSchema, QueryCachePolicySchema, FetchBlockSchema, AggregateBlockSchema, IncludeSubQueriesSchema, QueryDefinitionSchema, FilterValueSchema, FilterConditionSchema, LogicalOperatorSchema, FilterGroupSchema, ColumnConfigSchema, MetadataFieldConfigSchema, MetadataLineConfigSchema, TableSettingsSchema, ViewScopeSchema, ViewDSLSchema, ViewVisibilitySchema, ViewSourceSchema, SchemaViewDefinitionSchema, getComponentIdFromType, ComponentSchema, ContentTypeSchema, SettingsGroupSchema, SchemaDefinitionSchema;
2061
- var init_chunk_OOMXKJZA = __esm({
2062
- "../../domain/schema/dist/chunk-OOMXKJZA.js"() {
2265
+ var PARAMETER_EXPRESSION_SOURCE, ParameterExpressionStringSchema, QUERY_PARAMETER_TYPES, QueryParameterTypeSchema, QueryParameterScalarSchema, QueryParameterSchema, StringOperationSchema, StringOperationWithParamsSchema, ParameterizedLimitSchema, ParameterizedOffsetSchema, WhereValueSchema, WhereConditionSchema, SortConditionSchema, EnhancedIncludeObjectSchema, IncludeDefinitionSchema, SelectMappingSchema, FilterDefinitionSchema, FullTextSearchSchema, ContentTypesSchema, SearchBlockSchema, SimilarBlockSchema, AggregationMetricSchema, QueryCardinalitySchema, QueryCachePolicySchema, FetchBlockSchema, AggregateBlockSchema, IncludeSubQueriesSchema, QueryDefinitionSchema, VIEW_SLUG_PATTERN, VIEW_SLUG_MESSAGE, FilterValueSchema, FilterConditionSchema, LogicalOperatorSchema, FilterGroupSchema, ColumnConfigSchema, MetadataFieldConfigSchema, MetadataLineConfigSchema, TableSettingsSchema, ViewScopeSchema, ViewDSLSchema, ViewVisibilitySchema, ViewSourceSchema, SchemaViewDefinitionSchema, getComponentIdFromType, ComponentSchema, ContentTypeSchema, SettingsGroupSchema, SchemaDefinitionSchema;
2266
+ var init_chunk_2326XWZE = __esm({
2267
+ "../../domain/schema/dist/chunk-2326XWZE.js"() {
2063
2268
  init_chunk_VGBOPF6G();
2064
- init_chunk_NB2NTUXG();
2269
+ init_chunk_PCG6XHFD();
2270
+ PARAMETER_EXPRESSION_SOURCE = String.raw`\$\{([a-zA-Z][a-zA-Z0-9_]*)(\?([^}]*))?\}`;
2271
+ ParameterExpressionStringSchema = z.string().regex(
2272
+ new RegExp(`^${PARAMETER_EXPRESSION_SOURCE}$`),
2273
+ "Must be a parameter expression like ${limit?20}"
2274
+ );
2275
+ QUERY_PARAMETER_TYPES = [
2276
+ "string",
2277
+ "number",
2278
+ "integer",
2279
+ "boolean",
2280
+ "array",
2281
+ "object"
2282
+ ];
2283
+ QueryParameterTypeSchema = z.enum(QUERY_PARAMETER_TYPES);
2284
+ QueryParameterScalarSchema = z.union([z.string(), z.number(), z.boolean()]);
2285
+ QueryParameterSchema = z.object({
2286
+ name: z.string().regex(
2287
+ /^[a-zA-Z][a-zA-Z0-9_]*$/,
2288
+ "Parameter name must start with a letter and contain only letters, numbers, and underscores"
2289
+ ),
2290
+ type: QueryParameterTypeSchema.optional(),
2291
+ /** Absent means "required unless every `${…}` occurrence says otherwise". */
2292
+ required: z.boolean().optional(),
2293
+ default: z.union([QueryParameterScalarSchema, z.array(QueryParameterScalarSchema)]).optional(),
2294
+ minimum: z.number().optional(),
2295
+ maximum: z.number().optional(),
2296
+ minLength: z.number().int().min(0).optional(),
2297
+ maxLength: z.number().int().min(0).optional(),
2298
+ pattern: z.string().min(1).optional(),
2299
+ enum: z.array(QueryParameterScalarSchema).min(1).optional()
2300
+ });
2065
2301
  StringOperationSchema = z.enum([
2066
2302
  "first",
2067
2303
  // Get first character
@@ -2085,6 +2321,14 @@ var init_chunk_OOMXKJZA = __esm({
2085
2321
  name: z.string().min(1),
2086
2322
  defaultValue: z.union([z.string(), z.number(), z.boolean()]).optional()
2087
2323
  });
2324
+ ParameterizedLimitSchema = z.union([
2325
+ z.number().int().positive(),
2326
+ ParameterExpressionStringSchema
2327
+ ]);
2328
+ ParameterizedOffsetSchema = z.union([
2329
+ z.number().int().min(0),
2330
+ ParameterExpressionStringSchema
2331
+ ]);
2088
2332
  WhereValueSchema = z.union([
2089
2333
  z.string(),
2090
2334
  z.number(),
@@ -2232,8 +2476,8 @@ var init_chunk_OOMXKJZA = __esm({
2232
2476
  ]).optional(),
2233
2477
  // Sorting and pagination
2234
2478
  sort: SortConditionSchema.optional(),
2235
- limit: z.number().int().positive().optional(),
2236
- offset: z.number().int().min(0).optional()
2479
+ limit: ParameterizedLimitSchema.optional(),
2480
+ offset: ParameterizedOffsetSchema.optional()
2237
2481
  }).refine((data) => data.contentType || data.contentTypes, {
2238
2482
  message: "Either contentType or contentTypes is required for search queries"
2239
2483
  });
@@ -2250,7 +2494,7 @@ var init_chunk_OOMXKJZA = __esm({
2250
2494
  /** Fields to return */
2251
2495
  fields: SelectMappingSchema.optional(),
2252
2496
  /** Maximum results to return */
2253
- limit: z.number().int().positive().optional()
2497
+ limit: ParameterizedLimitSchema.optional()
2254
2498
  }).refine((data) => data.contentType || data.contentTypes, {
2255
2499
  message: "Either contentType or contentTypes is required for similar queries"
2256
2500
  });
@@ -2260,6 +2504,7 @@ var init_chunk_OOMXKJZA = __esm({
2260
2504
  z.string()
2261
2505
  // field expression like "items.quantity.sum" or "id.count"
2262
2506
  );
2507
+ QueryCardinalitySchema = z.enum(["one", "many"]);
2263
2508
  QueryCachePolicySchema = z.union([
2264
2509
  // Simple: "public 300s" or "private" or "public 300s swr 60s"
2265
2510
  z.string(),
@@ -2277,8 +2522,8 @@ var init_chunk_OOMXKJZA = __esm({
2277
2522
  where: WhereConditionSchema.optional(),
2278
2523
  include: IncludeDefinitionSchema.optional(),
2279
2524
  sort: SortConditionSchema.optional(),
2280
- limit: z.number().int().positive().optional(),
2281
- offset: z.number().int().min(0).optional()
2525
+ limit: ParameterizedLimitSchema.optional(),
2526
+ offset: ParameterizedOffsetSchema.optional()
2282
2527
  }).refine((data) => data.contentType || data.singleton, {
2283
2528
  message: "Either contentType or singleton must be specified"
2284
2529
  }).refine((data) => !(data.contentType && data.singleton), {
@@ -2323,7 +2568,7 @@ var init_chunk_OOMXKJZA = __esm({
2323
2568
  ])
2324
2569
  ).optional(),
2325
2570
  sort: SortConditionSchema.optional(),
2326
- limit: z.number().int().positive().optional()
2571
+ limit: ParameterizedLimitSchema.optional()
2327
2572
  });
2328
2573
  IncludeSubQueriesSchema = z.record(
2329
2574
  z.string(),
@@ -2331,7 +2576,7 @@ var init_chunk_OOMXKJZA = __esm({
2331
2576
  source: z.string(),
2332
2577
  where: WhereConditionSchema.optional(),
2333
2578
  sort: SortConditionSchema.optional(),
2334
- limit: z.number().int().positive().optional(),
2579
+ limit: ParameterizedLimitSchema.optional(),
2335
2580
  select: SelectMappingSchema.optional()
2336
2581
  })
2337
2582
  );
@@ -2359,7 +2604,15 @@ var init_chunk_OOMXKJZA = __esm({
2359
2604
  filters: FilterDefinitionSchema.optional(),
2360
2605
  fullText: FullTextSearchSchema.optional(),
2361
2606
  select: SelectMappingSchema.optional(),
2362
- cache: QueryCachePolicySchema.optional()
2607
+ cache: QueryCachePolicySchema.optional(),
2608
+ /**
2609
+ * What the query accepts. Additive and optional: a query that declares nothing still has the
2610
+ * parameters its own `${…}` expressions and endpoint segments declare — `declareQueryParameters`
2611
+ * is the one reader, and this block is authoritative where it speaks (type and constraints).
2612
+ */
2613
+ parameters: z.array(QueryParameterSchema).optional(),
2614
+ /** `one` or `many`. Absent means `many`, unless the query fetches a singleton. */
2615
+ cardinality: QueryCardinalitySchema.optional()
2363
2616
  }).refine((data) => data.fetch || data.aggregate || data.search || data.similar, {
2364
2617
  message: "Query must contain at least one of: fetch, aggregate, search, or similar block"
2365
2618
  }).refine(
@@ -2373,6 +2626,8 @@ var init_chunk_OOMXKJZA = __esm({
2373
2626
  message: "Aggregate block must have a source when not used with fetch"
2374
2627
  }
2375
2628
  );
2629
+ VIEW_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/;
2630
+ VIEW_SLUG_MESSAGE = "Slug must be lowercase, start with a letter, and contain only letters, numbers, and hyphens";
2376
2631
  FilterValueSchema = z.union([
2377
2632
  // Simple values
2378
2633
  z.string(),
@@ -2620,7 +2875,7 @@ var init_chunk_OOMXKJZA = __esm({
2620
2875
  * URL-friendly slug for the view (unique per dataset)
2621
2876
  * Used in Studio URLs like /studio/views/:slug
2622
2877
  */
2623
- slug: z.string().regex(/^[a-z][a-z0-9-]*$/, "Slug must be lowercase, start with a letter"),
2878
+ slug: z.string().regex(VIEW_SLUG_PATTERN, VIEW_SLUG_MESSAGE),
2624
2879
  /**
2625
2880
  * Optional description
2626
2881
  */
@@ -2676,7 +2931,7 @@ var init_chunk_OOMXKJZA = __esm({
2676
2931
  * URL-friendly slug for the view (optional, auto-generated from id if not provided)
2677
2932
  * Used in Studio URLs like /studio/views/:slug
2678
2933
  */
2679
- slug: z.string().regex(/^[a-z][a-z0-9-]*$/, "Slug must be lowercase, start with a letter").optional(),
2934
+ slug: z.string().regex(VIEW_SLUG_PATTERN, VIEW_SLUG_MESSAGE).optional(),
2680
2935
  /**
2681
2936
  * View name
2682
2937
  */
@@ -3138,18 +3393,19 @@ var init_chunk_OOMXKJZA = __esm({
3138
3393
  // ../../domain/schema/dist/parsed-type.js
3139
3394
  var init_parsed_type = __esm({
3140
3395
  "../../domain/schema/dist/parsed-type.js"() {
3141
- init_chunk_OOMXKJZA();
3396
+ init_chunk_2326XWZE();
3142
3397
  init_chunk_VGBOPF6G();
3143
- init_chunk_NB2NTUXG();
3398
+ init_chunk_PCG6XHFD();
3399
+ init_chunk_JH6U6ENN();
3144
3400
  }
3145
3401
  });
3146
3402
  async function loadRelyIOConfig(baseDir) {
3147
3403
  for (const configPath of CONFIG_FILE_PATHS) {
3148
- const fullPath = path3.join(baseDir, configPath);
3404
+ const fullPath = path5.join(baseDir, configPath);
3149
3405
  if (existsSync(fullPath)) {
3150
3406
  try {
3151
- const __filename2 = fileURLToPath(import.meta.url);
3152
- const jiti = createJiti(__filename2, {
3407
+ const __filename = fileURLToPath(import.meta.url);
3408
+ const jiti = createJiti(__filename, {
3153
3409
  interopDefault: true,
3154
3410
  // Must stay OFF: jiti's module cache is process-global, so in a long-running process
3155
3411
  // (`relyio dev`) a cached config would be re-served after edits. fsCache (content-hashed
@@ -3172,7 +3428,7 @@ async function loadRelyIOConfig(baseDir) {
3172
3428
  }
3173
3429
  async function findTypeScriptSchema(relyioDir) {
3174
3430
  for (const tsPath of TS_SCHEMA_PATHS) {
3175
- const fullPath = path3.join(relyioDir, tsPath);
3431
+ const fullPath = path5.join(relyioDir, tsPath);
3176
3432
  if (existsSync(fullPath)) {
3177
3433
  return {
3178
3434
  found: true,
@@ -3192,7 +3448,7 @@ async function findYamlSchemas(relyioDir) {
3192
3448
  if (yamlFiles.length > 0) {
3193
3449
  return {
3194
3450
  found: true,
3195
- paths: yamlFiles.map((f) => path3.join(relyioDir, f)),
3451
+ paths: yamlFiles.map((f) => path5.join(relyioDir, f)),
3196
3452
  description: `YAML schema (${yamlFiles.length} file${yamlFiles.length === 1 ? "" : "s"})`
3197
3453
  };
3198
3454
  }
@@ -3201,7 +3457,7 @@ async function findYamlSchemas(relyioDir) {
3201
3457
  return { found: false };
3202
3458
  }
3203
3459
  async function detectSchemaSource(baseDir = process.cwd()) {
3204
- const relyioDir = path3.join(baseDir, RELYIO_DIR);
3460
+ const relyioDir = path5.join(baseDir, RELYIO_DIR);
3205
3461
  if (!existsSync(relyioDir)) {
3206
3462
  return {
3207
3463
  sourceType: null,
@@ -3218,7 +3474,7 @@ async function detectSchemaSource(baseDir = process.cwd()) {
3218
3474
  if (config?.schemaSource) {
3219
3475
  if (config.schemaSource === "typescript") {
3220
3476
  if (config.schemaPath) {
3221
- const customPath = path3.join(baseDir, config.schemaPath);
3477
+ const customPath = path5.join(baseDir, config.schemaPath);
3222
3478
  if (existsSync(customPath)) {
3223
3479
  return {
3224
3480
  sourceType: "typescript",
@@ -3359,8 +3615,8 @@ async function loadTypeScriptSchema(schemaPath, detection) {
3359
3615
  });
3360
3616
  }
3361
3617
  try {
3362
- const __filename2 = fileURLToPath(import.meta.url);
3363
- const jiti = createJiti(__filename2, {
3618
+ const __filename = fileURLToPath(import.meta.url);
3619
+ const jiti = createJiti(__filename, {
3364
3620
  // Enable ESM interop
3365
3621
  interopDefault: true,
3366
3622
  // Must stay OFF. jiti's module cache is process-global and integrates with Node's require
@@ -3637,7 +3893,7 @@ function isIgnoredWatchPath(filePath) {
3637
3893
  return IGNORED_WATCH_DIR_RE.test(filePath);
3638
3894
  }
3639
3895
  async function getSchemaWatchPaths(baseDir = process.cwd()) {
3640
- const relyioDir = path3.join(baseDir, RELYIO_DIR);
3896
+ const relyioDir = path5.join(baseDir, RELYIO_DIR);
3641
3897
  return existsSync(relyioDir) ? [relyioDir] : [];
3642
3898
  }
3643
3899
  var RELYIO_DIR, TS_SCHEMA_PATHS, CONFIG_FILE_PATHS, SCHEMA_FILE_RE, DECLARATION_FILE_RE, IGNORED_WATCH_DIR_RE, SchemaNotFoundError;
@@ -3652,7 +3908,7 @@ var init_schema_loader = __esm({
3652
3908
  IGNORED_WATCH_DIR_RE = /(^|[/\\])(node_modules|\.git|dist|\.next|\.turbo|coverage)([/\\]|$)/;
3653
3909
  SchemaNotFoundError = class extends Error {
3654
3910
  constructor(baseDir) {
3655
- const relyioDir = path3.join(baseDir, RELYIO_DIR);
3911
+ const relyioDir = path5.join(baseDir, RELYIO_DIR);
3656
3912
  const message = `
3657
3913
  No schema found in ${relyioDir}
3658
3914
 
@@ -4060,13 +4316,214 @@ var init_deploy_service = __esm({
4060
4316
  };
4061
4317
  }
4062
4318
  });
4319
+
4320
+ // ../sdk/dist/chunk-XQ4QHWZE.js
4321
+ var __defProp2, __name;
4322
+ var init_chunk_XQ4QHWZE = __esm({
4323
+ "../sdk/dist/chunk-XQ4QHWZE.js"() {
4324
+ __defProp2 = Object.defineProperty;
4325
+ __name = (target, value) => __defProp2(target, "name", { value, configurable: true });
4326
+ }
4327
+ });
4328
+
4329
+ // ../sdk/dist/chunk-BBUB7C2Z.js
4330
+ function readDeadline(raw, now) {
4331
+ if (raw === void 0 || raw === null || raw === "") return { state: "none" };
4332
+ if (typeof raw !== "string") return { state: "unreadable" };
4333
+ const deadline = new Date(raw).getTime();
4334
+ if (!Number.isFinite(deadline)) return { state: "unreadable" };
4335
+ return deadline <= now ? { state: "passed", expiredAt: raw } : { state: "live", expiresAt: raw };
4336
+ }
4337
+ function parseDevContextFile(raw, now) {
4338
+ let parsed;
4339
+ try {
4340
+ parsed = JSON.parse(raw);
4341
+ } catch {
4342
+ return { state: "unreadable", reason: "not-json" };
4343
+ }
4344
+ if (typeof parsed !== "object" || parsed === null) {
4345
+ return { state: "unreadable", reason: "not-json" };
4346
+ }
4347
+ const record = parsed;
4348
+ const { datasetId, previewApiKey, branchId, expiresAt } = record;
4349
+ if (typeof datasetId !== "string" || datasetId === "" || typeof previewApiKey !== "string" || previewApiKey === "" || typeof branchId !== "string" || branchId === "") {
4350
+ return { state: "unreadable", reason: "missing-fields" };
4351
+ }
4352
+ const deadline = readDeadline(expiresAt, now);
4353
+ if (deadline.state === "unreadable") return { state: "unreadable", reason: "bad-expiry" };
4354
+ const context = {
4355
+ datasetId,
4356
+ previewApiKey,
4357
+ branchId,
4358
+ expiresAt: deadline.state === "none" ? null : expiresAt
4359
+ };
4360
+ return deadline.state === "passed" ? { state: "expired", context, expiredAt: deadline.expiredAt } : { state: "active", context };
4361
+ }
4362
+ function buildDevEnv(context) {
4363
+ const env = {
4364
+ [DEV_ENV.dataset]: context.datasetId,
4365
+ [DEV_ENV.apiKey]: context.previewApiKey
4366
+ };
4367
+ if (context.expiresAt != null && context.expiresAt !== "") {
4368
+ env[DEV_ENV.expiresAt] = context.expiresAt;
4369
+ }
4370
+ return env;
4371
+ }
4372
+ function parseDevEnv(env, now) {
4373
+ const dataset = env?.[DEV_ENV.dataset];
4374
+ const apiKey = env?.[DEV_ENV.apiKey];
4375
+ if (dataset == null || dataset === "" || apiKey == null || apiKey === "") {
4376
+ return { state: "absent" };
4377
+ }
4378
+ const deadline = readDeadline(env?.[DEV_ENV.expiresAt], now);
4379
+ switch (deadline.state) {
4380
+ case "unreadable":
4381
+ return { state: "unreadable", reason: "bad-expiry" };
4382
+ case "passed":
4383
+ return { state: "expired", expiredAt: deadline.expiredAt };
4384
+ case "live":
4385
+ return { state: "active", credentials: { dataset, apiKey }, expiresAt: deadline.expiresAt };
4386
+ case "none":
4387
+ return { state: "active", credentials: { dataset, apiKey }, expiresAt: null };
4388
+ }
4389
+ }
4390
+ var DEV_CONTEXT_DIR, DEV_CONTEXT_FILENAME, DEV_CONTEXT_FILE, DEV_ENV;
4391
+ var init_chunk_BBUB7C2Z = __esm({
4392
+ "../sdk/dist/chunk-BBUB7C2Z.js"() {
4393
+ init_chunk_XQ4QHWZE();
4394
+ DEV_CONTEXT_DIR = ".relyio";
4395
+ DEV_CONTEXT_FILENAME = "dev.json";
4396
+ DEV_CONTEXT_FILE = `${DEV_CONTEXT_DIR}/${DEV_CONTEXT_FILENAME}`;
4397
+ DEV_ENV = {
4398
+ dataset: "RELYIO_DEV_DATASET",
4399
+ apiKey: "RELYIO_DEV_API_KEY",
4400
+ expiresAt: "RELYIO_DEV_EXPIRES_AT"
4401
+ };
4402
+ __name(readDeadline, "readDeadline");
4403
+ __name(parseDevContextFile, "parseDevContextFile");
4404
+ __name(buildDevEnv, "buildDevEnv");
4405
+ __name(parseDevEnv, "parseDevEnv");
4406
+ }
4407
+ });
4408
+
4409
+ // ../sdk/dist/dev-handshake/index.js
4410
+ var init_dev_handshake = __esm({
4411
+ "../sdk/dist/dev-handshake/index.js"() {
4412
+ init_chunk_BBUB7C2Z();
4413
+ }
4414
+ });
4415
+ function registerPlugin(language, load3) {
4416
+ registeredPlugins.set(language, load3);
4417
+ }
4418
+ function registeredLanguages() {
4419
+ return [...registeredPlugins.keys()].sort((a, b) => a.localeCompare(b));
4420
+ }
4421
+ function asPlugin(module_, describe3) {
4422
+ const candidate = module_?.default ?? module_;
4423
+ const plugin3 = candidate;
4424
+ if (!plugin3 || typeof plugin3.generate !== "function") {
4425
+ throw new Error(`Invalid plugin: ${describe3}`);
4426
+ }
4427
+ return plugin3;
4428
+ }
4429
+ async function loadPlugin(language) {
4430
+ const registered = registeredPlugins.get(language);
4431
+ if (!registered) {
4432
+ const available = registeredLanguages();
4433
+ throw new Error(
4434
+ `No generator is registered for "${language}". ` + (available.length > 0 ? `Registered generators: ${available.join(", ")}.` : `No generator is registered at all \u2014 the host registers them at startup.`)
4435
+ );
4436
+ }
4437
+ return asPlugin(await registered(), `registered ${language}`);
4438
+ }
4439
+ async function writeFiles(files, outputDir) {
4440
+ await fs4.mkdir(outputDir, { recursive: true });
4441
+ await Promise.all(
4442
+ files.map(async (file) => {
4443
+ const fullPath = path5.join(outputDir, file.path);
4444
+ const dir = path5.dirname(fullPath);
4445
+ await fs4.mkdir(dir, { recursive: true });
4446
+ await fs4.writeFile(fullPath, file.content, "utf8");
4447
+ })
4448
+ );
4449
+ }
4450
+ function words(identifier) {
4451
+ return identifier.split(/[-_\s]+/).filter((word) => word.length > 0);
4452
+ }
4453
+ var BUILT_IN_LANGUAGES, registeredPlugins, TemplateEngine, naming;
4454
+ var init_dist2 = __esm({
4455
+ "../codegen-core/dist/index.js"() {
4456
+ BUILT_IN_LANGUAGES = ["typescript", "python"];
4457
+ registeredPlugins = /* @__PURE__ */ new Map();
4458
+ TemplateEngine = class {
4459
+ handlebars;
4460
+ constructor() {
4461
+ this.handlebars = Handlebars.create();
4462
+ this.registerHelpers();
4463
+ }
4464
+ registerHelpers() {
4465
+ this.handlebars.registerHelper("pascalCase", (str) => {
4466
+ if (!str) return "";
4467
+ return str.replaceAll(/(^|_)(\w)/g, (_, __, char) => char.toUpperCase());
4468
+ });
4469
+ this.handlebars.registerHelper("camelCase", (str) => {
4470
+ if (!str) return "";
4471
+ const pascal = str.replaceAll(/(^|_)(\w)/g, (_, __, char) => char.toUpperCase());
4472
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
4473
+ });
4474
+ this.handlebars.registerHelper("snakeCase", (str) => {
4475
+ if (!str) return "";
4476
+ return str.replaceAll(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
4477
+ });
4478
+ this.handlebars.registerHelper("kebabCase", (str) => {
4479
+ if (!str) return "";
4480
+ return str.replaceAll(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
4481
+ });
4482
+ this.handlebars.registerHelper("plural", (str) => {
4483
+ if (!str) return "";
4484
+ if (str.endsWith("y")) {
4485
+ return str.slice(0, -1) + "ies";
4486
+ }
4487
+ if (str.endsWith("s") || str.endsWith("sh") || str.endsWith("ch")) {
4488
+ return str + "es";
4489
+ }
4490
+ return str + "s";
4491
+ });
4492
+ }
4493
+ compile(template) {
4494
+ const compiledTemplate = this.handlebars.compile(template);
4495
+ return (context) => compiledTemplate(context);
4496
+ }
4497
+ render(template, context) {
4498
+ const compiledTemplate = this.compile(template);
4499
+ return compiledTemplate(context);
4500
+ }
4501
+ };
4502
+ new TemplateEngine();
4503
+ naming = {
4504
+ /** `blogPost` → `BlogPost`. The name of a generated type, class or struct. */
4505
+ pascal(identifier) {
4506
+ return words(identifier).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
4507
+ },
4508
+ /** `blog_post` → `blogPost`. The name of a generated value or field. */
4509
+ camel(identifier) {
4510
+ const pascal = naming.pascal(identifier);
4511
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
4512
+ },
4513
+ /** `blogPost` → `blog_post`. Python module paths and field names. */
4514
+ snake(identifier) {
4515
+ return identifier.replaceAll(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "").replaceAll(/[-\s]+/g, "_");
4516
+ }
4517
+ };
4518
+ }
4519
+ });
4063
4520
  function normalizeReferenceTargets(to) {
4064
4521
  if (to == null) return [];
4065
4522
  return Array.isArray(to) ? [...to] : [to];
4066
4523
  }
4067
4524
  var BaseFieldConfigSchema, ReferenceStrengthSchema, OnTargetDeleteSchema, ReferenceTargetSchema;
4068
- var init_chunk_YL47KAP2 = __esm({
4069
- "../../domain/schema/dist/chunk-YL47KAP2.js"() {
4525
+ var init_chunk_RIJEP5Z6 = __esm({
4526
+ "../../domain/schema/dist/chunk-RIJEP5Z6.js"() {
4070
4527
  BaseFieldConfigSchema = z.object({
4071
4528
  required: z.boolean().optional(),
4072
4529
  unique: z.boolean().optional(),
@@ -4120,10 +4577,11 @@ var init_chunk_YL47KAP2 = __esm({
4120
4577
  });
4121
4578
  }
4122
4579
  });
4123
- var init_chunk_KWUTF26B = __esm({
4124
- "../../domain/schema/dist/chunk-KWUTF26B.js"() {
4125
- init_chunk_YL47KAP2();
4126
- init_chunk_NB2NTUXG();
4580
+ var init_chunk_3UOSTM65 = __esm({
4581
+ "../../domain/schema/dist/chunk-3UOSTM65.js"() {
4582
+ init_chunk_2326XWZE();
4583
+ init_chunk_RIJEP5Z6();
4584
+ init_chunk_PCG6XHFD();
4127
4585
  BaseFieldConfigSchema.extend({
4128
4586
  maxLength: z.number().min(1).optional(),
4129
4587
  pattern: z.string().optional(),
@@ -4359,30 +4817,62 @@ var init_chunk_XTMEVLMJ = __esm({
4359
4817
  }
4360
4818
  });
4361
4819
 
4820
+ // ../../domain/schema/dist/chunk-73FJJM22.js
4821
+ var TABLE_SYSTEM_FIELD_KINDS;
4822
+ var init_chunk_73FJJM22 = __esm({
4823
+ "../../domain/schema/dist/chunk-73FJJM22.js"() {
4824
+ init_chunk_RIJEP5Z6();
4825
+ init_chunk_PCG6XHFD();
4826
+ TABLE_SYSTEM_FIELD_KINDS = {
4827
+ id: "text",
4828
+ contentType: "choice",
4829
+ status: "text",
4830
+ createdAt: "date",
4831
+ updatedAt: "date",
4832
+ publishedAt: "date",
4833
+ createdBy: "reference",
4834
+ updatedBy: "reference"
4835
+ };
4836
+ ({
4837
+ ...TABLE_SYSTEM_FIELD_KINDS,
4838
+ "file.status": "text",
4839
+ "file.backend": "text",
4840
+ "file.mimeType": "text",
4841
+ "file.fileName": "text",
4842
+ "file.size": "number",
4843
+ "file.extracted.width": "number",
4844
+ "file.extracted.height": "number",
4845
+ "file.extracted.duration": "number"
4846
+ });
4847
+ }
4848
+ });
4849
+
4362
4850
  // ../../domain/schema/dist/chunk-QSWR2YJN.js
4363
4851
  var init_chunk_QSWR2YJN = __esm({
4364
4852
  "../../domain/schema/dist/chunk-QSWR2YJN.js"() {
4365
4853
  }
4366
4854
  });
4367
4855
 
4368
- // ../../domain/schema/dist/chunk-N5M2EZW6.js
4369
- var init_chunk_N5M2EZW6 = __esm({
4370
- "../../domain/schema/dist/chunk-N5M2EZW6.js"() {
4371
- init_chunk_YL47KAP2();
4856
+ // ../../domain/schema/dist/chunk-2ABFISBQ.js
4857
+ var init_chunk_2ABFISBQ = __esm({
4858
+ "../../domain/schema/dist/chunk-2ABFISBQ.js"() {
4859
+ init_chunk_RIJEP5Z6();
4372
4860
  init_chunk_QSWR2YJN();
4373
- init_chunk_NB2NTUXG();
4861
+ init_chunk_PCG6XHFD();
4374
4862
  }
4375
4863
  });
4376
- var init_dist = __esm({
4864
+ var init_dist3 = __esm({
4377
4865
  "../../domain/schema/dist/index.js"() {
4378
- init_chunk_KWUTF26B();
4379
- init_chunk_OOMXKJZA();
4866
+ init_chunk_3UOSTM65();
4867
+ init_chunk_2326XWZE();
4380
4868
  init_chunk_XTMEVLMJ();
4381
4869
  init_chunk_VGBOPF6G();
4382
- init_chunk_N5M2EZW6();
4383
- init_chunk_YL47KAP2();
4870
+ init_chunk_73FJJM22();
4871
+ init_chunk_2ABFISBQ();
4872
+ init_chunk_RIJEP5Z6();
4384
4873
  init_chunk_QSWR2YJN();
4385
- init_chunk_NB2NTUXG();
4874
+ init_chunk_PCG6XHFD();
4875
+ init_chunk_JH6U6ENN();
4386
4876
  new Set(FORBIDDEN_ASSET_TYPE_FIELD_TYPES);
4387
4877
  }
4388
4878
  });
@@ -4427,24 +4917,17 @@ function validateSchema(schema) {
4427
4917
  formatted: SchemaValidator.formatErrors(errors)
4428
4918
  };
4429
4919
  }
4430
- function toPascalCase(str) {
4431
- const spaced = str.replaceAll(/([a-z])([A-Z])/g, "$1 $2");
4432
- return spaced.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
4433
- }
4434
- function toCamelCase(str) {
4435
- const pascal = toPascalCase(str);
4436
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
4437
- }
4438
4920
  async function generateTypeScriptFromSchema(rawSchema, options) {
4439
4921
  const parsedSchema = parseSchema(rawSchema);
4440
4922
  const generator = new TypeScriptGenerator(parsedSchema, options);
4441
4923
  return generator.generate();
4442
4924
  }
4443
4925
  var ASSET_KINDS2, SchemaParser, SchemaValidator, TypeScriptGenerator, plugin, index_default;
4444
- var init_dist2 = __esm({
4926
+ var init_dist4 = __esm({
4445
4927
  "../codegen-typescript/dist/index.js"() {
4446
- init_dist();
4447
- init_dist();
4928
+ init_dist3();
4929
+ init_dist2();
4930
+ init_dist3();
4448
4931
  ASSET_KINDS2 = ["image", "video", "file"];
4449
4932
  SchemaParser = class {
4450
4933
  /**
@@ -4911,16 +5394,16 @@ var init_dist2 = __esm({
4911
5394
  * Validate a content type
4912
5395
  */
4913
5396
  validateContentType(id, contentType) {
4914
- const path23 = `contentTypes.${id}`;
4915
- this.validateFields(contentType.fields, path23);
5397
+ const path25 = `contentTypes.${id}`;
5398
+ this.validateFields(contentType.fields, path25);
4916
5399
  if (contentType.fields.length === 0) {
4917
- this.addWarning(path23, "Content type has no fields defined");
5400
+ this.addWarning(path25, "Content type has no fields defined");
4918
5401
  }
4919
5402
  const reservedFields = /* @__PURE__ */ new Set(["id", "createdAt", "updatedAt", "_id"]);
4920
5403
  for (const field of contentType.fields) {
4921
5404
  if (reservedFields.has(field.id)) {
4922
5405
  this.addError(
4923
- `${path23}.fields.${field.id}`,
5406
+ `${path25}.fields.${field.id}`,
4924
5407
  `Field name '${field.id}' is reserved by BaseContent`,
4925
5408
  `Use a different field name`
4926
5409
  );
@@ -4947,20 +5430,20 @@ var init_dist2 = __esm({
4947
5430
  /**
4948
5431
  * Validate field type
4949
5432
  */
4950
- validateFieldType(field, path23) {
5433
+ validateFieldType(field, path25) {
4951
5434
  if (field.type === "enum" && (!field.enumValues || field.enumValues.length === 0)) {
4952
- this.addError(path23, "Enum field has no values defined");
5435
+ this.addError(path25, "Enum field has no values defined");
4953
5436
  }
4954
5437
  if (field.type === "reference" && !field.referenceType) {
4955
5438
  this.addError(
4956
- path23,
5439
+ path25,
4957
5440
  "Reference field missing referenceType",
4958
5441
  "Ensure the reference target (to) is specified in the schema"
4959
5442
  );
4960
5443
  }
4961
5444
  if (field.type === "component" && !field.componentType) {
4962
5445
  this.addError(
4963
- path23,
5446
+ path25,
4964
5447
  "Component field missing componentType",
4965
5448
  "Ensure component type is specified as component(componentName)"
4966
5449
  );
@@ -4970,11 +5453,11 @@ var init_dist2 = __esm({
4970
5453
  * Check for circular references
4971
5454
  */
4972
5455
  checkCircularReferences(schema) {
4973
- const checkType = (typeName, visited, path23) => {
5456
+ const checkType = (typeName, visited, path25) => {
4974
5457
  if (visited.has(typeName)) {
4975
5458
  this.addError(
4976
- path23.join(" -> "),
4977
- `Circular reference detected: ${path23.join(" -> ")} -> ${typeName}`
5459
+ path25.join(" -> "),
5460
+ `Circular reference detected: ${path25.join(" -> ")} -> ${typeName}`
4978
5461
  );
4979
5462
  return;
4980
5463
  }
@@ -4983,7 +5466,7 @@ var init_dist2 = __esm({
4983
5466
  visited.add(typeName);
4984
5467
  for (const field of contentType.fields) {
4985
5468
  if (field.isReference && field.referenceType) {
4986
- checkType(field.referenceType, new Set(visited), [...path23, field.referenceType]);
5469
+ checkType(field.referenceType, new Set(visited), [...path25, field.referenceType]);
4987
5470
  }
4988
5471
  }
4989
5472
  };
@@ -4994,10 +5477,10 @@ var init_dist2 = __esm({
4994
5477
  /**
4995
5478
  * Add an error
4996
5479
  */
4997
- addError(path23, message, suggestion) {
5480
+ addError(path25, message, suggestion) {
4998
5481
  this.errors.push({
4999
5482
  type: "error",
5000
- path: path23,
5483
+ path: path25,
5001
5484
  message,
5002
5485
  suggestion
5003
5486
  });
@@ -5005,10 +5488,10 @@ var init_dist2 = __esm({
5005
5488
  /**
5006
5489
  * Add a warning
5007
5490
  */
5008
- addWarning(path23, message, suggestion) {
5491
+ addWarning(path25, message, suggestion) {
5009
5492
  this.warnings.push({
5010
5493
  type: "warning",
5011
- path: path23,
5494
+ path: path25,
5012
5495
  message,
5013
5496
  suggestion
5014
5497
  });
@@ -5153,6 +5636,9 @@ ${prefix}: ${error2.path}`, ` ${error2.message}`);
5153
5636
  import type {
5154
5637
  ${typeImports.join(",\n ")},
5155
5638
  } from '@relyio/sdk/schema'
5639
+
5640
+ // Puts '@relyio/sdk' in the program for the \`Register\` augmentation at the end of this file
5641
+ import type {} from '@relyio/sdk'
5156
5642
  `);
5157
5643
  }
5158
5644
  /**
@@ -5183,7 +5669,7 @@ import type {
5183
5669
  }
5184
5670
  lines.push(" */");
5185
5671
  }
5186
- const varName = toCamelCase(component.id) + "Component";
5672
+ const varName = naming.camel(component.id) + "Component";
5187
5673
  lines.push(`export const ${varName} = defineComponent('${component.id}', {`);
5188
5674
  if (component.displayName) {
5189
5675
  lines.push(` displayName: '${this.escapeString(component.displayName)}',`);
@@ -5226,7 +5712,7 @@ import type {
5226
5712
  }
5227
5713
  lines.push(" */");
5228
5714
  }
5229
- const varName = toCamelCase(contentType.id);
5715
+ const varName = naming.camel(contentType.id);
5230
5716
  lines.push(`export const ${varName} = defineContentType('${contentType.id}', {`);
5231
5717
  if (contentType.displayName) {
5232
5718
  lines.push(` displayName: '${this.escapeString(contentType.displayName)}',`);
@@ -5283,7 +5769,7 @@ import type {
5283
5769
  }
5284
5770
  lines.push(" */");
5285
5771
  }
5286
- const varName = toCamelCase(assetType.id);
5772
+ const varName = naming.camel(assetType.id);
5287
5773
  lines.push(`export const ${varName} = defineAssetType('${assetType.id}', {`);
5288
5774
  if (assetType.displayName) {
5289
5775
  lines.push(` displayName: '${this.escapeString(assetType.displayName)}',`);
@@ -5558,7 +6044,7 @@ import type {
5558
6044
  }
5559
6045
  lines.push(" */");
5560
6046
  }
5561
- const varName = toCamelCase(singleton5.id);
6047
+ const varName = naming.camel(singleton5.id);
5562
6048
  lines.push(`export const ${varName} = defineSingleton('${singleton5.id}', {`);
5563
6049
  if (singleton5.displayName) {
5564
6050
  lines.push(` displayName: '${this.escapeString(singleton5.displayName)}',`);
@@ -5610,7 +6096,7 @@ import type {
5610
6096
  }
5611
6097
  lines.push(" */");
5612
6098
  }
5613
- const varName = toCamelCase(group.id) + "Settings";
6099
+ const varName = naming.camel(group.id) + "Settings";
5614
6100
  lines.push(`export const ${varName} = defineSettingsGroup('${group.id}', {`);
5615
6101
  if (group.displayName) {
5616
6102
  lines.push(` displayName: '${this.escapeString(group.displayName)}',`);
@@ -5643,7 +6129,7 @@ import type {
5643
6129
  */
5644
6130
  generateQuery(query) {
5645
6131
  const lines = [];
5646
- const varName = toCamelCase(query.id) + "Query";
6132
+ const varName = naming.camel(query.id) + "Query";
5647
6133
  lines.push(`export const ${varName} = defineQuery('${query.id}', {`);
5648
6134
  if (query.endpoint) {
5649
6135
  lines.push(` endpoint: '${query.endpoint}',`);
@@ -5702,7 +6188,7 @@ import type {
5702
6188
  if (this.schema.contentTypes.size > 0) {
5703
6189
  lines.push(" contentTypes: {");
5704
6190
  for (const [id] of this.schema.contentTypes) {
5705
- const varName = toCamelCase(id);
6191
+ const varName = naming.camel(id);
5706
6192
  lines.push(` ${id}: ${varName},`);
5707
6193
  }
5708
6194
  lines.push(" },");
@@ -5710,7 +6196,7 @@ import type {
5710
6196
  if (this.schema.components.size > 0) {
5711
6197
  lines.push(" components: {");
5712
6198
  for (const [id] of this.schema.components) {
5713
- const varName = toCamelCase(id) + "Component";
6199
+ const varName = naming.camel(id) + "Component";
5714
6200
  lines.push(` ${id}: ${varName},`);
5715
6201
  }
5716
6202
  lines.push(" },");
@@ -5718,7 +6204,7 @@ import type {
5718
6204
  if (this.schema.assetTypes.size > 0) {
5719
6205
  lines.push(" assetTypes: {");
5720
6206
  for (const [id] of this.schema.assetTypes) {
5721
- const varName = toCamelCase(id);
6207
+ const varName = naming.camel(id);
5722
6208
  lines.push(` ${id}: ${varName},`);
5723
6209
  }
5724
6210
  lines.push(" },");
@@ -5726,7 +6212,7 @@ import type {
5726
6212
  if (this.schema.singletons.size > 0) {
5727
6213
  lines.push(" singletonContentTypes: {");
5728
6214
  for (const [id] of this.schema.singletons) {
5729
- const varName = toCamelCase(id);
6215
+ const varName = naming.camel(id);
5730
6216
  lines.push(` ${id}: ${varName},`);
5731
6217
  }
5732
6218
  lines.push(" },");
@@ -5734,7 +6220,7 @@ import type {
5734
6220
  if (this.schema.queries.size > 0) {
5735
6221
  lines.push(" queries: {");
5736
6222
  for (const [id] of this.schema.queries) {
5737
- const varName = toCamelCase(id) + "Query";
6223
+ const varName = naming.camel(id) + "Query";
5738
6224
  lines.push(` ${id}: ${varName},`);
5739
6225
  }
5740
6226
  lines.push(" },");
@@ -5742,7 +6228,7 @@ import type {
5742
6228
  if (this.schema.settingsGroups.size > 0) {
5743
6229
  lines.push(" settingsGroups: {");
5744
6230
  for (const [id] of this.schema.settingsGroups) {
5745
- const varName = toCamelCase(id) + "Settings";
6231
+ const varName = naming.camel(id) + "Settings";
5746
6232
  lines.push(` ${id}: ${varName},`);
5747
6233
  }
5748
6234
  lines.push(" },");
@@ -5760,13 +6246,13 @@ import type {
5760
6246
  `);
5761
6247
  const lines = [];
5762
6248
  for (const [id] of this.schema.contentTypes) {
5763
- const varName = toCamelCase(id);
5764
- const typeName = toPascalCase(id);
6249
+ const varName = naming.camel(id);
6250
+ const typeName = naming.pascal(id);
5765
6251
  lines.push(`export type ${typeName} = InferContentType<typeof ${varName}>`);
5766
6252
  }
5767
6253
  for (const [, assetType] of this.schema.assetTypes) {
5768
- const varName = toCamelCase(assetType.id);
5769
- const typeName = toPascalCase(assetType.id);
6254
+ const varName = naming.camel(assetType.id);
6255
+ const typeName = naming.pascal(assetType.id);
5770
6256
  lines.push(`export type ${typeName} = InferAssetType<typeof ${varName}>`);
5771
6257
  if (assetType.variantIds && assetType.variantIds.length > 0) {
5772
6258
  const union = assetType.variantIds.map((vid) => `'${this.escapeString(vid)}'`).join(" | ");
@@ -5774,8 +6260,8 @@ import type {
5774
6260
  }
5775
6261
  }
5776
6262
  for (const [id] of this.schema.singletons) {
5777
- const varName = toCamelCase(id);
5778
- const typeName = toPascalCase(id);
6263
+ const varName = naming.camel(id);
6264
+ const typeName = naming.pascal(id);
5779
6265
  lines.push(`export type ${typeName} = InferSingleton<typeof ${varName}>`);
5780
6266
  }
5781
6267
  this.output.push(
@@ -5785,6 +6271,27 @@ import type {
5785
6271
  export type SchemaTypes = typeof schema
5786
6272
  `
5787
6273
  );
6274
+ this.addRegisterAugmentation();
6275
+ }
6276
+ /**
6277
+ * Register this schema with the SDK, so `createClient({ apiKey })` is typed with no generic and no
6278
+ * `schema:` (`INV-SS-TYPES-INFERRED`, `sdk-surfaces-spec.md` decision 7).
6279
+ *
6280
+ * It is emitted into this same file rather than a second one: the augmentation takes effect
6281
+ * wherever `schema.ts` is part of the program. It needs `'@relyio/sdk'` itself in the program too,
6282
+ * which is what the empty `import type {} from '@relyio/sdk'` in `addImports` is for — measured,
6283
+ * a program whose only SDK import is `@relyio/sdk/client` fails this block with TS2664 ("module
6284
+ * '@relyio/sdk' cannot be found") even though the name resolves. Two generated files augmenting
6285
+ * `Register` in one program conflict on `schema` (TS2717); one program registers one schema.
6286
+ */
6287
+ addRegisterAugmentation() {
6288
+ this.output.push(`// Registers this schema with the SDK: \`createClient({ apiKey })\` is typed by it
6289
+ declare module '@relyio/sdk' {
6290
+ interface Register {
6291
+ schema: typeof schema
6292
+ }
6293
+ }
6294
+ `);
5788
6295
  }
5789
6296
  /**
5790
6297
  * Escape string for use in generated code
@@ -5918,18 +6425,12 @@ function mapFieldTypeToPython(fieldType, config) {
5918
6425
  default: {
5919
6426
  const componentMatch = fieldType.match(/^component\(([^)]+)\)$/);
5920
6427
  if (componentMatch) {
5921
- return toPascalCase2(componentMatch[1]);
6428
+ return naming.pascal(componentMatch[1]);
5922
6429
  }
5923
6430
  return "Any";
5924
6431
  }
5925
6432
  }
5926
6433
  }
5927
- function toPascalCase2(str) {
5928
- return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
5929
- }
5930
- function toSnakeCase(str) {
5931
- return str.replaceAll(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "").replaceAll(/[-\s]+/g, "_");
5932
- }
5933
6434
  function isRecord2(value) {
5934
6435
  return value !== null && typeof value === "object" && !Array.isArray(value);
5935
6436
  }
@@ -5968,7 +6469,7 @@ async function generatePython(ir, options) {
5968
6469
  for (const [id, contentType] of recordEntries(ir.contentTypes)) {
5969
6470
  const content = generateContentType(id, asSchemaType(contentType), options);
5970
6471
  files.push({
5971
- path: `content_types/${toSnakeCase(id)}.py`,
6472
+ path: `content_types/${naming.snake(id)}.py`,
5972
6473
  content
5973
6474
  });
5974
6475
  }
@@ -5982,7 +6483,7 @@ async function generatePython(ir, options) {
5982
6483
  for (const [id, component] of recordEntries(ir.components)) {
5983
6484
  const content = generateComponent(id, asSchemaType(component), options);
5984
6485
  files.push({
5985
- path: `components/${toSnakeCase(id)}.py`,
6486
+ path: `components/${naming.snake(id)}.py`,
5986
6487
  content
5987
6488
  });
5988
6489
  }
@@ -5996,7 +6497,7 @@ async function generatePython(ir, options) {
5996
6497
  for (const [id, singleton5] of recordEntries(ir.singletons)) {
5997
6498
  const content = generateSingleton(id, asSchemaType(singleton5), options);
5998
6499
  files.push({
5999
- path: `singletons/${toSnakeCase(id)}.py`,
6500
+ path: `singletons/${naming.snake(id)}.py`,
6000
6501
  content
6001
6502
  });
6002
6503
  }
@@ -6109,7 +6610,7 @@ __all__ = ['RichTextContent', 'ImageField', 'FileField', 'Reference']
6109
6610
  `;
6110
6611
  }
6111
6612
  function generateContentType(id, contentType, options) {
6112
- const className = toPascalCase2(id);
6613
+ const className = naming.pascal(id);
6113
6614
  const fields = generateFields(contentType.fields || [], options);
6114
6615
  const imports = ["from ..base_types import RichTextContent, ImageField, FileField, Reference"];
6115
6616
  if (options.baseModel === "pydantic") {
@@ -6147,7 +6648,7 @@ __all__ = ['${className}', 'Create${className}Input', 'Update${className}Input']
6147
6648
  }
6148
6649
  }
6149
6650
  function generateComponent(id, component, options) {
6150
- const className = toPascalCase2(id);
6651
+ const className = naming.pascal(id);
6151
6652
  const fields = generateFields(component.fields || [], options);
6152
6653
  const imports = ["from ..base_types import RichTextContent, ImageField, FileField, Reference"];
6153
6654
  if (options.baseModel === "pydantic") {
@@ -6178,7 +6679,7 @@ __all__ = ['${className}']
6178
6679
  }
6179
6680
  }
6180
6681
  function generateSingleton(id, singleton5, options) {
6181
- const className = toPascalCase2(id);
6682
+ const className = naming.pascal(id);
6182
6683
  const fields = generateFields(singleton5.fields || [], options);
6183
6684
  const imports = ["from ..base_types import RichTextContent, ImageField, FileField, Reference"];
6184
6685
  if (options.baseModel === "pydantic") {
@@ -6214,12 +6715,12 @@ __all__ = ['${className}', 'Update${className}Input']
6214
6715
  }
6215
6716
  function generateFields(fields, options) {
6216
6717
  return fields.map((field) => {
6217
- const fieldName = toSnakeCase(field.id);
6718
+ const fieldName = naming.snake(field.id);
6218
6719
  const pythonType = mapFieldTypeToPython(field.type, field.config);
6219
6720
  const isOptional = !field.required;
6220
6721
  const fieldType = isOptional ? `Optional[${pythonType}]` : pythonType;
6221
6722
  const defaultValue = isOptional ? " = None" : "";
6222
- if (options.baseModel === "pydantic" && field.id !== toSnakeCase(field.id)) {
6723
+ if (options.baseModel === "pydantic" && field.id !== naming.snake(field.id)) {
6223
6724
  return `${fieldName}: ${fieldType} = Field(${defaultValue.slice(3)}, alias='${field.id}')`;
6224
6725
  }
6225
6726
  return `${fieldName}: ${fieldType}${defaultValue}`;
@@ -6339,8 +6840,8 @@ class TenantContext(BaseModel):
6339
6840
  `);
6340
6841
  if (isRecord2(ir.contentTypes)) {
6341
6842
  for (const id of Object.keys(ir.contentTypes)) {
6342
- const className = toPascalCase2(id);
6343
- imports.push(`from .content_types.${toSnakeCase(id)} import ${className}`);
6843
+ const className = naming.pascal(id);
6844
+ imports.push(`from .content_types.${naming.snake(id)} import ${className}`);
6344
6845
  tenantTypes.push(`
6345
6846
  class Tenant${className}(${className}):
6346
6847
  """Tenant-scoped ${className}"""
@@ -6359,8 +6860,8 @@ class TenantContext:
6359
6860
  `);
6360
6861
  if (isRecord2(ir.contentTypes)) {
6361
6862
  for (const id of Object.keys(ir.contentTypes)) {
6362
- const className = toPascalCase2(id);
6363
- imports.push(`from .content_types.${toSnakeCase(id)} import ${className}`);
6863
+ const className = naming.pascal(id);
6864
+ imports.push(`from .content_types.${naming.snake(id)} import ${className}`);
6364
6865
  tenantTypes.push(`
6365
6866
  @dataclass
6366
6867
  class Tenant${className}(${className}):
@@ -6373,7 +6874,7 @@ class Tenant${className}(${className}):
6373
6874
  const exportList = ["TenantContext"];
6374
6875
  if (isRecord2(ir.contentTypes)) {
6375
6876
  for (const id of Object.keys(ir.contentTypes)) {
6376
- exportList.push(`Tenant${toPascalCase2(id)}`);
6877
+ exportList.push(`Tenant${naming.pascal(id)}`);
6377
6878
  }
6378
6879
  }
6379
6880
  return `${imports.join("\n")}
@@ -6416,10 +6917,10 @@ __all__ = ['validate_data', 'to_dict', 'to_json']
6416
6917
  }
6417
6918
  function generateContentTypesIndex(contentTypeIds, _options) {
6418
6919
  const imports = contentTypeIds.map(
6419
- (id) => `from .${toSnakeCase(id)} import ${toPascalCase2(id)}, Create${toPascalCase2(id)}Input, Update${toPascalCase2(id)}Input`
6920
+ (id) => `from .${naming.snake(id)} import ${naming.pascal(id)}, Create${naming.pascal(id)}Input, Update${naming.pascal(id)}Input`
6420
6921
  );
6421
6922
  const exports = contentTypeIds.flatMap((id) => {
6422
- const className = toPascalCase2(id);
6923
+ const className = naming.pascal(id);
6423
6924
  return [`'${className}'`, `'Create${className}Input'`, `'Update${className}Input'`];
6424
6925
  });
6425
6926
  return `${imports.join("\n")}
@@ -6428,8 +6929,8 @@ __all__ = [${exports.join(", ")}]
6428
6929
  `;
6429
6930
  }
6430
6931
  function generateComponentsIndex(componentIds, _options) {
6431
- const imports = componentIds.map((id) => `from .${toSnakeCase(id)} import ${toPascalCase2(id)}`);
6432
- const exports = componentIds.map((id) => `'${toPascalCase2(id)}'`);
6932
+ const imports = componentIds.map((id) => `from .${naming.snake(id)} import ${naming.pascal(id)}`);
6933
+ const exports = componentIds.map((id) => `'${naming.pascal(id)}'`);
6433
6934
  return `${imports.join("\n")}
6434
6935
 
6435
6936
  __all__ = [${exports.join(", ")}]
@@ -6437,10 +6938,10 @@ __all__ = [${exports.join(", ")}]
6437
6938
  }
6438
6939
  function generateSingletonsIndex(singletonIds, _options) {
6439
6940
  const imports = singletonIds.map(
6440
- (id) => `from .${toSnakeCase(id)} import ${toPascalCase2(id)}, Update${toPascalCase2(id)}Input`
6941
+ (id) => `from .${naming.snake(id)} import ${naming.pascal(id)}, Update${naming.pascal(id)}Input`
6441
6942
  );
6442
6943
  const exports = singletonIds.flatMap((id) => {
6443
- const className = toPascalCase2(id);
6944
+ const className = naming.pascal(id);
6444
6945
  return [`'${className}'`, `'Update${className}Input'`];
6445
6946
  });
6446
6947
  return `${imports.join("\n")}
@@ -6449,8 +6950,10 @@ __all__ = [${exports.join(", ")}]
6449
6950
  `;
6450
6951
  }
6451
6952
  var plugin2, index_default2;
6452
- var init_dist3 = __esm({
6953
+ var init_dist5 = __esm({
6453
6954
  "../codegen-python/dist/index.js"() {
6955
+ init_dist2();
6956
+ init_dist2();
6454
6957
  plugin2 = {
6455
6958
  language: "python",
6456
6959
  async generate(ir, options) {
@@ -6862,7 +7365,8 @@ async function fetchHandoffSummary(req) {
6862
7365
  body: JSON.stringify({
6863
7366
  fromAgent: req.fromAgent,
6864
7367
  toAgent: req.toAgent,
6865
- messages: req.messages
7368
+ messages: req.messages,
7369
+ projectId: req.projectId
6866
7370
  }),
6867
7371
  signal: controller.signal
6868
7372
  });
@@ -8045,7 +8549,7 @@ var init_agent_loop = __esm({
8045
8549
  async tryReadFile(relPath) {
8046
8550
  if (!this.projectRoot) return void 0;
8047
8551
  try {
8048
- const fullPath = path3.resolve(this.projectRoot, relPath);
8552
+ const fullPath = path5.resolve(this.projectRoot, relPath);
8049
8553
  if (!fullPath.startsWith(this.projectRoot)) return void 0;
8050
8554
  return await readFile(fullPath, "utf8");
8051
8555
  } catch {
@@ -8206,7 +8710,9 @@ var init_agent_loop = __esm({
8206
8710
  const result = await fetchHandoffSummary({
8207
8711
  fromAgent,
8208
8712
  toAgent,
8209
- messages: this.messages
8713
+ messages: this.messages,
8714
+ // The gateway needs it to attribute the summary's tokens and cost to an org.
8715
+ projectId: this.options.context.projectId
8210
8716
  });
8211
8717
  if (result) {
8212
8718
  this.messages = [
@@ -8302,10 +8808,10 @@ var init_permission_gate = __esm({
8302
8808
  return PROTECTED_PATHS.some((p) => normalized.startsWith(p) || normalized.includes(`/${p}`));
8303
8809
  }
8304
8810
  isOutsideProjectRoot(filePath) {
8305
- if (path3.isAbsolute(filePath)) {
8811
+ if (path5.isAbsolute(filePath)) {
8306
8812
  return !filePath.startsWith(this.projectRoot);
8307
8813
  }
8308
- const resolved = path3.resolve(this.projectRoot, filePath);
8814
+ const resolved = path5.resolve(this.projectRoot, filePath);
8309
8815
  return !resolved.startsWith(this.projectRoot);
8310
8816
  }
8311
8817
  };
@@ -8414,8 +8920,8 @@ var init_context_provider = __esm({
8414
8920
  }
8415
8921
  async getCurrentGitBranch() {
8416
8922
  try {
8417
- const { execSync: execSync3 } = await import('child_process');
8418
- const branch = execSync3("git rev-parse --abbrev-ref HEAD", {
8923
+ const { execSync: execSync2 } = await import('child_process');
8924
+ const branch = execSync2("git rev-parse --abbrev-ref HEAD", {
8419
8925
  encoding: "utf8",
8420
8926
  stdio: ["pipe", "pipe", "pipe"]
8421
8927
  }).trim();
@@ -8454,7 +8960,7 @@ var init_context_provider = __esm({
8454
8960
  });
8455
8961
  const schemaFiles = [];
8456
8962
  for (const filePath of files) {
8457
- const fullPath = path3.join(projectRoot, filePath);
8963
+ const fullPath = path5.join(projectRoot, filePath);
8458
8964
  try {
8459
8965
  const stats = await stat2(fullPath);
8460
8966
  const contentType = this.inferContentType(filePath);
@@ -8522,7 +9028,7 @@ var init_read_file_handler = __esm({
8522
9028
  }
8523
9029
  async execute(input) {
8524
9030
  const filePath = input.path;
8525
- const fullPath = path3.resolve(this.projectRoot, filePath);
9031
+ const fullPath = path5.resolve(this.projectRoot, filePath);
8526
9032
  if (!fullPath.startsWith(this.projectRoot)) {
8527
9033
  return { error: "Path is outside project root" };
8528
9034
  }
@@ -8605,12 +9111,12 @@ var init_write_file_handler = __esm({
8605
9111
  async execute(input) {
8606
9112
  const filePath = input.path;
8607
9113
  const content = input.content;
8608
- const fullPath = path3.resolve(this.projectRoot, filePath);
9114
+ const fullPath = path5.resolve(this.projectRoot, filePath);
8609
9115
  if (!fullPath.startsWith(this.projectRoot)) {
8610
9116
  return { error: "Path is outside project root" };
8611
9117
  }
8612
9118
  try {
8613
- await mkdir(path3.dirname(fullPath), { recursive: true });
9119
+ await mkdir(path5.dirname(fullPath), { recursive: true });
8614
9120
  await writeFile(fullPath, content, "utf8");
8615
9121
  return { success: true, path: filePath, bytesWritten: content.length };
8616
9122
  } catch (error2) {
@@ -8634,7 +9140,7 @@ var init_apply_patch_handler = __esm({
8634
9140
  const filePath = input.path;
8635
9141
  const searchBlock = input.searchBlock;
8636
9142
  const replaceBlock = input.replaceBlock;
8637
- const fullPath = path3.resolve(this.projectRoot, filePath);
9143
+ const fullPath = path5.resolve(this.projectRoot, filePath);
8638
9144
  if (!fullPath.startsWith(this.projectRoot)) {
8639
9145
  return { error: "Path is outside project root" };
8640
9146
  }
@@ -10142,7 +10648,7 @@ function Banner() {
10142
10648
  label: "App",
10143
10649
  value: dev.sandbox.wiredEnv ? "wired" : "manual",
10144
10650
  valueColor: dev.sandbox.wiredEnv ? "green" : void 0,
10145
- suffix: dev.sandbox.wiredEnv ? void 0 : ".relyio/dev.json"
10651
+ suffix: dev.sandbox.wiredEnv ? void 0 : DEV_CONTEXT_FILE
10146
10652
  }
10147
10653
  ];
10148
10654
  } else {
@@ -10208,11 +10714,12 @@ function FieldRow({ field }) {
10208
10714
  var CLI_VERSION2, BRAND_GLYPH, BRAND_NAME, LABEL_WIDTH, TWO_COLUMN_MIN_COLS;
10209
10715
  var init_banner = __esm({
10210
10716
  "src/ai/repl/components/banner.tsx"() {
10717
+ init_dev_handshake();
10211
10718
  init_runtime_context();
10212
10719
  init_dev_cockpit_context();
10213
10720
  init_dev_cockpit_format();
10214
10721
  init_startup_info();
10215
- CLI_VERSION2 = "0.2.2" ;
10722
+ CLI_VERSION2 = "0.3.1" ;
10216
10723
  BRAND_GLYPH = "\u25B6";
10217
10724
  BRAND_NAME = "RelyIO";
10218
10725
  LABEL_WIDTH = 9;
@@ -10472,8 +10979,8 @@ function entryLabel(fields, contentType) {
10472
10979
  return title || contentType || "";
10473
10980
  }
10474
10981
  function itemTarget(tc) {
10475
- const path23 = tc.input.path;
10476
- if (path23) return path23;
10982
+ const path25 = tc.input.path;
10983
+ if (path25) return path25;
10477
10984
  switch (tc.toolName) {
10478
10985
  case "getSchemaReference": {
10479
10986
  const topic = tc.input.topic;
@@ -11201,8 +11708,8 @@ var init_input_preprocessor = __esm({
11201
11708
  var HISTORY_DIR, HISTORY_FILE, MAX_HISTORY_ENTRIES, PersistentHistory;
11202
11709
  var init_input_history = __esm({
11203
11710
  "src/ai/services/input-history.ts"() {
11204
- HISTORY_DIR = path3.join(os.homedir(), ".relyio");
11205
- HISTORY_FILE = path3.join(HISTORY_DIR, "ai-history");
11711
+ HISTORY_DIR = path5.join(os.homedir(), ".relyio");
11712
+ HISTORY_FILE = path5.join(HISTORY_DIR, "ai-history");
11206
11713
  MAX_HISTORY_ENTRIES = 1e3;
11207
11714
  PersistentHistory = class {
11208
11715
  entries = [];
@@ -11213,7 +11720,7 @@ var init_input_history = __esm({
11213
11720
  }
11214
11721
  load() {
11215
11722
  try {
11216
- fs2.mkdirSync(path3.dirname(this.filePath), { recursive: true });
11723
+ fs2.mkdirSync(path5.dirname(this.filePath), { recursive: true });
11217
11724
  if (fs2.existsSync(this.filePath)) {
11218
11725
  const content = fs2.readFileSync(this.filePath, "utf8");
11219
11726
  this.entries = content.split("\n").filter(Boolean);
@@ -11243,7 +11750,7 @@ var init_input_history = __esm({
11243
11750
  }
11244
11751
  save() {
11245
11752
  try {
11246
- fs2.mkdirSync(path3.dirname(this.filePath), { recursive: true });
11753
+ fs2.mkdirSync(path5.dirname(this.filePath), { recursive: true });
11247
11754
  fs2.writeFileSync(this.filePath, this.entries.join("\n") + "\n");
11248
11755
  } catch {
11249
11756
  }
@@ -11252,18 +11759,18 @@ var init_input_history = __esm({
11252
11759
  }
11253
11760
  });
11254
11761
  function readMentionedFile(filePath, projectRoot) {
11255
- const resolved = path3.isAbsolute(filePath) ? filePath : path3.resolve(projectRoot, filePath);
11762
+ const resolved = path5.isAbsolute(filePath) ? filePath : path5.resolve(projectRoot, filePath);
11256
11763
  try {
11257
11764
  const stat2 = fs2.statSync(resolved);
11258
11765
  if (stat2.size > MAX_FILE_SIZE) {
11259
11766
  const content2 = fs2.readFileSync(resolved, "utf8").slice(0, MAX_FILE_SIZE);
11260
11767
  const lines2 = content2.split("\n").length;
11261
- const relativePath2 = path3.relative(projectRoot, resolved);
11768
+ const relativePath2 = path5.relative(projectRoot, resolved);
11262
11769
  return { filePath: relativePath2, content: content2, lines: lines2, truncated: true };
11263
11770
  }
11264
11771
  const content = fs2.readFileSync(resolved, "utf8");
11265
11772
  const lines = content.split("\n").length;
11266
- const relativePath = path3.relative(projectRoot, resolved);
11773
+ const relativePath = path5.relative(projectRoot, resolved);
11267
11774
  return { filePath: relativePath, content, lines, truncated: false };
11268
11775
  } catch {
11269
11776
  return null;
@@ -12135,11 +12642,11 @@ var init_autocomplete = __esm({
12135
12642
  const schemaMatches = this.schemaFiles.filter((f) => f.toLowerCase().includes(query.toLowerCase())).slice(0, 15).map((f) => ({ value: f, label: f, kind: "file" }));
12136
12643
  if (schemaMatches.length > 0) return schemaMatches;
12137
12644
  try {
12138
- const dir = query.includes("/") ? path3.join(this.projectRoot, path3.dirname(query)) : this.projectRoot;
12139
- const prefix = query.includes("/") ? path3.basename(query) : query;
12645
+ const dir = query.includes("/") ? path5.join(this.projectRoot, path5.dirname(query)) : this.projectRoot;
12646
+ const prefix = query.includes("/") ? path5.basename(query) : query;
12140
12647
  const entries = fs2.readdirSync(dir, { withFileTypes: true });
12141
12648
  return entries.filter((e) => e.name.startsWith(prefix) && !e.name.startsWith(".")).map((e) => {
12142
- const rel = path3.relative(this.projectRoot, path3.join(dir, e.name));
12649
+ const rel = path5.relative(this.projectRoot, path5.join(dir, e.name));
12143
12650
  const value = e.isDirectory() ? `${rel}/` : rel;
12144
12651
  return { value, label: value, kind: "file" };
12145
12652
  }).slice(0, 15);
@@ -12600,8 +13107,7 @@ function ModelPicker() {
12600
13107
  description = `${description} \xB7 now routing to ${resolvedModelId}`;
12601
13108
  }
12602
13109
  if (!m.available) {
12603
- const reason = m.disabledReason === "paused" ? "AI is globally paused" : "admin-disabled for this workspace";
12604
- description = `${description} \xB7 ${reason}`;
13110
+ description = `${description} \xB7 admin-disabled for this workspace`;
12605
13111
  }
12606
13112
  return {
12607
13113
  id: m.id,
@@ -13991,7 +14497,7 @@ var init_use_confirmation = __esm({
13991
14497
  var SESSIONS_DIR, SESSION_VERSION, SessionManager;
13992
14498
  var init_session_manager = __esm({
13993
14499
  "src/ai/services/session-manager.ts"() {
13994
- SESSIONS_DIR = path3.join(os.homedir(), ".relyio", "ai-sessions");
14500
+ SESSIONS_DIR = path5.join(os.homedir(), ".relyio", "ai-sessions");
13995
14501
  SESSION_VERSION = 4;
13996
14502
  SessionManager = class {
13997
14503
  sessionsDir;
@@ -14000,13 +14506,13 @@ var init_session_manager = __esm({
14000
14506
  fs2.mkdirSync(this.sessionsDir, { recursive: true });
14001
14507
  }
14002
14508
  save(session) {
14003
- const filePath = path3.join(this.sessionsDir, `${session.id}.json`);
14509
+ const filePath = path5.join(this.sessionsDir, `${session.id}.json`);
14004
14510
  session.version = SESSION_VERSION;
14005
14511
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
14006
14512
  fs2.writeFileSync(filePath, JSON.stringify(session, null, 2));
14007
14513
  }
14008
14514
  load(sessionId) {
14009
- const filePath = path3.join(this.sessionsDir, `${sessionId}.json`);
14515
+ const filePath = path5.join(this.sessionsDir, `${sessionId}.json`);
14010
14516
  try {
14011
14517
  const content = fs2.readFileSync(filePath, "utf8");
14012
14518
  const parsed = JSON.parse(content);
@@ -14063,7 +14569,7 @@ var init_session_manager = __esm({
14063
14569
  const files = fs2.readdirSync(this.sessionsDir).filter((f) => f.endsWith(".json"));
14064
14570
  return files.map((f) => {
14065
14571
  try {
14066
- const content = fs2.readFileSync(path3.join(this.sessionsDir, f), "utf8");
14572
+ const content = fs2.readFileSync(path5.join(this.sessionsDir, f), "utf8");
14067
14573
  const parsed = JSON.parse(content);
14068
14574
  if (parsed.version !== SESSION_VERSION && parsed.version !== 3 && parsed.version !== 2) {
14069
14575
  return null;
@@ -14081,7 +14587,7 @@ var init_session_manager = __esm({
14081
14587
  return this.listAll().slice(0, count);
14082
14588
  }
14083
14589
  delete(sessionId) {
14084
- const filePath = path3.join(this.sessionsDir, `${sessionId}.json`);
14590
+ const filePath = path5.join(this.sessionsDir, `${sessionId}.json`);
14085
14591
  try {
14086
14592
  fs2.unlinkSync(filePath);
14087
14593
  return true;
@@ -14119,7 +14625,7 @@ var init_snapshot_store = __esm({
14119
14625
  SnapshotStore = class {
14120
14626
  dir;
14121
14627
  constructor(sessionDir) {
14122
- this.dir = path3.join(sessionDir, "snapshots");
14628
+ this.dir = path5.join(sessionDir, "snapshots");
14123
14629
  fs2.mkdirSync(this.dir, { recursive: true });
14124
14630
  }
14125
14631
  /**
@@ -14180,7 +14686,7 @@ var init_snapshot_store = __esm({
14180
14686
  const hash = entry.slice(0, -4);
14181
14687
  if (keepHashes.has(hash)) continue;
14182
14688
  try {
14183
- fs2.unlinkSync(path3.join(this.dir, entry));
14689
+ fs2.unlinkSync(path5.join(this.dir, entry));
14184
14690
  removed++;
14185
14691
  } catch {
14186
14692
  }
@@ -14188,7 +14694,7 @@ var init_snapshot_store = __esm({
14188
14694
  return removed;
14189
14695
  }
14190
14696
  pathFor(hash) {
14191
- return path3.join(this.dir, `${hash}.bin`);
14697
+ return path5.join(this.dir, `${hash}.bin`);
14192
14698
  }
14193
14699
  };
14194
14700
  }
@@ -14203,7 +14709,7 @@ var init_decision_log = __esm({
14203
14709
  DecisionLog = class {
14204
14710
  filePath;
14205
14711
  constructor(sessionDir) {
14206
- this.filePath = path3.join(sessionDir, "decisions.jsonl");
14712
+ this.filePath = path5.join(sessionDir, "decisions.jsonl");
14207
14713
  if (!fs2.existsSync(this.filePath)) {
14208
14714
  fs2.writeFileSync(this.filePath, "", "utf8");
14209
14715
  }
@@ -14328,7 +14834,7 @@ var init_session_history = __esm({
14328
14834
  "src/ai/sessions/session-history.ts"() {
14329
14835
  init_snapshot_store();
14330
14836
  init_decision_log();
14331
- SESSIONS_ROOT = path3.join(os.homedir(), ".relyio", "ai-sessions");
14837
+ SESSIONS_ROOT = path5.join(os.homedir(), ".relyio", "ai-sessions");
14332
14838
  MAX_UNDOABLE_EVENTS = 200;
14333
14839
  SessionHistory = class {
14334
14840
  sessionDir;
@@ -14337,7 +14843,7 @@ var init_session_history = __esm({
14337
14843
  projectRoot;
14338
14844
  constructor(args) {
14339
14845
  const root = args.sessionsRoot ?? SESSIONS_ROOT;
14340
- this.sessionDir = path3.join(root, args.sessionId);
14846
+ this.sessionDir = path5.join(root, args.sessionId);
14341
14847
  fs2.mkdirSync(this.sessionDir, { recursive: true });
14342
14848
  this.snapshots = new SnapshotStore(this.sessionDir);
14343
14849
  this.log = new DecisionLog(this.sessionDir);
@@ -14498,11 +15004,11 @@ var init_session_history = __esm({
14498
15004
  * files via a malformed log entry.
14499
15005
  */
14500
15006
  async writeFileSafe(relPath, content) {
14501
- const fullPath = path3.resolve(this.projectRoot, relPath);
15007
+ const fullPath = path5.resolve(this.projectRoot, relPath);
14502
15008
  if (!fullPath.startsWith(this.projectRoot)) {
14503
15009
  throw new Error(`Refusing to write outside project root: ${relPath}`);
14504
15010
  }
14505
- await fs2.promises.mkdir(path3.dirname(fullPath), { recursive: true });
15011
+ await fs2.promises.mkdir(path5.dirname(fullPath), { recursive: true });
14506
15012
  await fs2.promises.writeFile(fullPath, content, "utf8");
14507
15013
  }
14508
15014
  };
@@ -14510,10 +15016,10 @@ var init_session_history = __esm({
14510
15016
  });
14511
15017
  function renameLocalSessionArtifacts(oldId, newId) {
14512
15018
  if (oldId === newId) return;
14513
- const oldTranscript = path3.join(SESSIONS_DIR2, `${oldId}.json`);
14514
- const newTranscript = path3.join(SESSIONS_DIR2, `${newId}.json`);
14515
- const oldHistoryDir = path3.join(SESSIONS_DIR2, oldId);
14516
- const newHistoryDir = path3.join(SESSIONS_DIR2, newId);
15019
+ const oldTranscript = path5.join(SESSIONS_DIR2, `${oldId}.json`);
15020
+ const newTranscript = path5.join(SESSIONS_DIR2, `${newId}.json`);
15021
+ const oldHistoryDir = path5.join(SESSIONS_DIR2, oldId);
15022
+ const newHistoryDir = path5.join(SESSIONS_DIR2, newId);
14517
15023
  try {
14518
15024
  if (fs2.existsSync(oldTranscript) && !fs2.existsSync(newTranscript)) {
14519
15025
  fs2.renameSync(oldTranscript, newTranscript);
@@ -14617,7 +15123,7 @@ var init_use_session_persistence = __esm({
14617
15123
  init_runtime_context();
14618
15124
  init_session_manager();
14619
15125
  init_session_history();
14620
- SESSIONS_DIR2 = path3.join(os.homedir(), ".relyio", "ai-sessions");
15126
+ SESSIONS_DIR2 = path5.join(os.homedir(), ".relyio", "ai-sessions");
14621
15127
  }
14622
15128
  });
14623
15129
 
@@ -14639,8 +15145,8 @@ var init_proposals_client = __esm({
14639
15145
  init_gateway_url();
14640
15146
  AI_GATEWAY_PROPOSALS_PATH = "/ai/proposals";
14641
15147
  ProposalsClient = class {
14642
- async request(method, path23, init = {}) {
14643
- const url = new URL(`${getGatewayUrl()}${AI_GATEWAY_PROPOSALS_PATH}${path23}`);
15148
+ async request(method, path25, init = {}) {
15149
+ const url = new URL(`${getGatewayUrl()}${AI_GATEWAY_PROPOSALS_PATH}${path25}`);
14644
15150
  if (init.query) {
14645
15151
  for (const [k, v] of Object.entries(init.query)) url.searchParams.set(k, v);
14646
15152
  }
@@ -14656,7 +15162,7 @@ var init_proposals_client = __esm({
14656
15162
  const raw = await response.text().catch(() => "");
14657
15163
  const refusal = readError(parseJson(raw));
14658
15164
  throw new Error(
14659
- refusal === void 0 ? `Proposals API ${method} ${path23} \u2192 ${response.status}: ${raw}` : `Proposals API ${method} ${path23} \u2192 ${response.status} ${refusal.code}: ${refusal.messages.join(" ")}`
15165
+ refusal === void 0 ? `Proposals API ${method} ${path25} \u2192 ${response.status}: ${raw}` : `Proposals API ${method} ${path25} \u2192 ${response.status} ${refusal.code}: ${refusal.messages.join(" ")}`
14660
15166
  );
14661
15167
  }
14662
15168
  return readPayload(await response.json());
@@ -15434,8 +15940,8 @@ function formatHistoryEntries(entries, status, limit) {
15434
15940
  const rows = entries.map((e) => {
15435
15941
  const time = new Date(e.timestamp).toLocaleTimeString();
15436
15942
  const glyph = decisionGlyph(e.kind);
15437
- const path23 = e.path || "\u2014";
15438
- return ` ${time} ${glyph} ${path23}`;
15943
+ const path25 = e.path || "\u2014";
15944
+ return ` ${time} ${glyph} ${path25}`;
15439
15945
  });
15440
15946
  return [header, "", ...rows, "", footer].join("\n");
15441
15947
  }
@@ -15673,11 +16179,10 @@ Tip: just type \`/model\` (no args) to pick interactively.`
15673
16179
  return;
15674
16180
  }
15675
16181
  if (!match.available) {
15676
- const reason = match.disabledReason === "paused" ? "AI is paused globally" : "this model is currently disabled by the workspace admin";
15677
16182
  ctx.dispatch({
15678
16183
  type: "SYSTEM_MESSAGE_APPENDED",
15679
16184
  label: "/model",
15680
- text: `Cannot switch to ${match.name} \u2014 ${reason}.`
16185
+ text: `Cannot switch to ${match.name} \u2014 this model is currently disabled by the workspace admin.`
15681
16186
  });
15682
16187
  return;
15683
16188
  }
@@ -16063,7 +16568,7 @@ async function resolveContext(spec, flags, pins = {}) {
16063
16568
  acc.datasetId = pins.datasetId;
16064
16569
  acc.datasetName = await safeDatasetName(projectId, acc.branchId, pins.datasetId);
16065
16570
  } else {
16066
- const datasetOutcome = await resolveDataset(projectId, acc.branchId, flags.dataset);
16571
+ const datasetOutcome = await resolveDataset2(projectId, acc.branchId, flags.dataset);
16067
16572
  if (datasetOutcome.kind === "needs-pick") {
16068
16573
  return { ...datasetOutcome, partial: acc };
16069
16574
  }
@@ -16137,7 +16642,7 @@ async function safeBranchName(projectId, branchId) {
16137
16642
  return void 0;
16138
16643
  }
16139
16644
  }
16140
- async function resolveDataset(projectId, branchId, flag) {
16645
+ async function resolveDataset2(projectId, branchId, flag) {
16141
16646
  let datasets;
16142
16647
  try {
16143
16648
  datasets = await manage_default.getProjectDatasets(projectId, { branchId });
@@ -16717,223 +17222,55 @@ function confirm(message) {
16717
17222
  // src/api/auth.ts
16718
17223
  init_client();
16719
17224
  init_config();
17225
+ init_dist();
16720
17226
  var isDebug3 = () => process.env.DEBUG === "true" || process.env.DEBUG === "1";
16721
- var HTML_TEMPLATES = {
16722
- success: (tokenName) => `
16723
- <html>
16724
- <head>
16725
- <title>RelyIO Authentication</title>
16726
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
16727
- <style>
16728
- body {
16729
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16730
- text-align: center;
16731
- padding: 2rem;
16732
- max-width: 600px;
16733
- margin: 0 auto;
16734
- line-height: 1.5;
16735
- color: #333;
16736
- }
16737
- h1 { color: #38a169; margin-bottom: 1rem; }
16738
- p { margin-bottom: 1rem; }
16739
- </style>
16740
- </head>
16741
- <body>
16742
- <h1>Authentication Successful</h1>
16743
- <p>${tokenName ? `Personal access token "${tokenName}" created successfully.` : "You have successfully authenticated."}</p>
16744
- <p>You can close this window and return to the CLI.</p>
16745
- </body>
16746
- </html>
16747
- `,
16748
- error: (message) => `
16749
- <html>
16750
- <head>
16751
- <title>RelyIO Authentication Error</title>
16752
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
16753
- <style>
16754
- body {
16755
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16756
- text-align: center;
16757
- padding: 2rem;
16758
- max-width: 600px;
16759
- margin: 0 auto;
16760
- line-height: 1.5;
16761
- color: #333;
16762
- }
16763
- h1 { color: #e53e3e; margin-bottom: 1rem; }
16764
- p { margin-bottom: 1rem; }
16765
- </style>
16766
- </head>
16767
- <body>
16768
- <h1>Authentication Failed</h1>
16769
- <p>${message}</p>
16770
- <p>Please try again or contact support if the issue persists.</p>
16771
- </body>
16772
- </html>
16773
- `,
16774
- serverError: (message) => `
16775
- <html>
16776
- <head>
16777
- <title>RelyIO Server Error</title>
16778
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
16779
- <style>
16780
- body {
16781
- font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
16782
- text-align: center;
16783
- padding: 2rem;
16784
- max-width: 600px;
16785
- margin: 0 auto;
16786
- line-height: 1.5;
16787
- color: #333;
16788
- }
16789
- h1 { color: #e53e3e; margin-bottom: 1rem; }
16790
- p { margin-bottom: 1rem; }
16791
- </style>
16792
- </head>
16793
- <body>
16794
- <h1>Internal Error</h1>
16795
- <p>An unexpected error occurred during authentication:</p>
16796
- <p>${message}</p>
16797
- <p>Please try again or contact support if the issue persists.</p>
16798
- </body>
16799
- </html>
16800
- `
16801
- };
17227
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
16802
17228
  async function login(email, password) {
16803
17229
  const response = await client_default.post("/auth/login", { email, password });
16804
17230
  await config_default.storeAuthToken(response.token);
16805
17231
  return response;
16806
17232
  }
16807
17233
  async function browserLogin() {
16808
- const server = http.createServer();
16809
- await new Promise((resolve) => server.listen(0, resolve));
16810
- const { port } = server.address();
16811
- const callbackUrl = `http://localhost:${port}`;
16812
- const deviceName = os__default.hostname();
16813
17234
  const dashboardUrl = config_default.getDashboardUrl();
16814
17235
  if (!dashboardUrl) {
16815
17236
  throw new Error(
16816
17237
  "Dashboard URL is not configured. Please run the command with --dashboard-url option."
16817
17238
  );
16818
17239
  }
16819
- const authUrl = `${dashboardUrl}/auth/cli-login?callback=${encodeURIComponent(callbackUrl)}&device_name=${encodeURIComponent(deviceName)}`;
17240
+ const state = randomBytes(32).toString("base64url");
17241
+ const server = await startCallbackServer({ callbackPath: "/" });
16820
17242
  try {
16821
- await open(authUrl);
16822
- console.log(success("Browser opened successfully"));
16823
- } catch (error2) {
16824
- console.error(error(`Failed to open browser: ${error2.message}`));
16825
- throw new Error(`Failed to open browser: ${error2.message}`);
16826
- }
16827
- console.log(info("Waiting for authentication to complete..."));
16828
- const tokenData = await new Promise((resolve, reject) => {
16829
- const timeoutId = setTimeout(
16830
- () => {
16831
- cleanupServer(server);
16832
- reject(new Error("Authentication timed out. Please try again."));
16833
- },
16834
- 5 * 60 * 1e3
16835
- );
16836
- let authProcessed = false;
16837
- server.on("request", async (req, res) => {
16838
- try {
16839
- const reqUrl = new URL(req.url || "", `http://localhost:${port}`);
16840
- const params = reqUrl.searchParams;
16841
- const token = params.get("token");
16842
- const expires_at = params.get("expires_at");
16843
- const token_name = params.get("token_name");
16844
- if (!authProcessed && token) {
16845
- authProcessed = true;
16846
- clearTimeout(timeoutId);
16847
- try {
16848
- await (token_name && expires_at ? config_default.storePAT(token, token_name, expires_at) : config_default.storeAuthToken(token));
16849
- sendHtmlResponse(res, 200, HTML_TEMPLATES.success(token_name || void 0));
16850
- let user;
16851
- try {
16852
- user = await getCurrentUser();
16853
- } catch (userError) {
16854
- if (isDebug3()) console.error("[relyio-cli][debug] fetch user failed:", userError);
16855
- user = {
16856
- id: "unknown",
16857
- email: "unknown",
16858
- name: "Unknown User"
16859
- };
16860
- }
16861
- cleanupServer(server);
16862
- resolve({
16863
- token,
16864
- user
16865
- });
16866
- } catch (storageError) {
16867
- if (isDebug3()) console.error("[relyio-cli][debug] token storage:", storageError);
16868
- sendHtmlResponse(
16869
- res,
16870
- 500,
16871
- HTML_TEMPLATES.serverError("Failed to store authentication token")
16872
- );
16873
- cleanupServer(server);
16874
- reject(storageError);
16875
- }
16876
- } else if (!authProcessed && params.has("error")) {
16877
- authProcessed = true;
16878
- const errorMsg = params.get("error") || "Authentication failed or was canceled.";
16879
- sendHtmlResponse(res, 400, HTML_TEMPLATES.error(errorMsg));
16880
- cleanupServer(server);
16881
- reject(new Error(errorMsg));
16882
- } else {
16883
- if (!token && !authProcessed) {
16884
- if (!res.headersSent) {
16885
- res.writeHead(200, { "Content-Type": "text/plain" });
16886
- res.end("Waiting for authentication to complete...");
16887
- }
16888
- } else if (authProcessed && !res.headersSent) {
16889
- res.writeHead(200, { "Content-Type": "text/plain" });
16890
- res.end("Authentication already processed. You can close this window.");
16891
- }
16892
- }
16893
- } catch (error2) {
16894
- if (isDebug3()) console.error("[relyio-cli][debug] callback processing:", error2);
16895
- if (authProcessed) {
16896
- if (!res.headersSent) {
16897
- res.writeHead(500, { "Content-Type": "text/plain" });
16898
- res.end("Error occurred, but authentication was already processed.");
16899
- }
16900
- } else {
16901
- authProcessed = true;
16902
- if (!res.headersSent) {
16903
- sendHtmlResponse(
16904
- res,
16905
- 500,
16906
- HTML_TEMPLATES.serverError(error2.message || "Unknown error")
16907
- );
16908
- }
16909
- cleanupServer(server);
16910
- reject(error2);
16911
- }
16912
- }
16913
- });
16914
- });
16915
- return tokenData;
16916
- }
16917
- function sendHtmlResponse(res, statusCode, html) {
16918
- if (!res.headersSent) {
16919
- res.writeHead(statusCode, {
16920
- "Content-Type": "text/html; charset=utf-8",
16921
- "Cache-Control": "no-store, no-cache, must-revalidate"
16922
- });
16923
- res.end(html);
17243
+ const authUrl = new URL("/auth/cli-login", dashboardUrl);
17244
+ authUrl.searchParams.set("callback", server.redirectUri);
17245
+ authUrl.searchParams.set("device_name", os__default.hostname());
17246
+ authUrl.searchParams.set("state", state);
17247
+ try {
17248
+ await open(authUrl.toString());
17249
+ console.log(success("Browser opened successfully"));
17250
+ } catch (error2) {
17251
+ console.error(error(`Failed to open browser: ${error2.message}`));
17252
+ throw new Error(`Failed to open browser: ${error2.message}`);
17253
+ }
17254
+ console.log(info("Waiting for authentication to complete..."));
17255
+ const handOff = await server.waitForCallback(state, LOGIN_TIMEOUT_MS);
17256
+ const token = handOff.get("token");
17257
+ const tokenName = handOff.get("token_name");
17258
+ const expiresAt = handOff.get("expires_at");
17259
+ if (token === null || token === "") {
17260
+ throw new Error("The sign-in callback carried no token.");
17261
+ }
17262
+ await (tokenName && expiresAt ? config_default.storePAT(token, tokenName, expiresAt) : config_default.storeAuthToken(token));
17263
+ return { token, user: await currentUserOrStub() };
17264
+ } finally {
17265
+ server.close();
16924
17266
  }
16925
17267
  }
16926
- function cleanupServer(server) {
17268
+ async function currentUserOrStub() {
16927
17269
  try {
16928
- server.close();
16929
- if (typeof server.closeAllConnections === "function") {
16930
- server.closeAllConnections();
16931
- }
16932
- const exitTimeout = setTimeout(() => {
16933
- }, 100);
16934
- exitTimeout.unref();
16935
- } catch (error2) {
16936
- if (isDebug3()) console.error("[relyio-cli][debug] server close:", error2);
17270
+ return await getCurrentUser();
17271
+ } catch (userError) {
17272
+ if (isDebug3()) console.error("[relyio-cli][debug] fetch user failed:", userError);
17273
+ return { id: "unknown", email: "unknown", name: "Unknown User" };
16937
17274
  }
16938
17275
  }
16939
17276
  async function logout() {
@@ -17020,18 +17357,17 @@ init_client();
17020
17357
  async function getProjects() {
17021
17358
  return await client_default.get("/projects");
17022
17359
  }
17023
- var __filename = fileURLToPath(import.meta.url);
17024
- var __dirname = path3.dirname(__filename);
17360
+ var __dirname = path5.dirname(fileURLToPath(import.meta.url));
17025
17361
  function getTemplatesDir() {
17026
17362
  const override = process.env.RELYIO_CLI_TEMPLATES_DIR;
17027
17363
  const candidates = [
17028
- override ? path3.resolve(override) : null,
17364
+ override ? path5.resolve(override) : null,
17029
17365
  // dist/index.js → ../templates (built layout)
17030
- path3.resolve(__dirname, "..", "templates"),
17031
- // src/commands/init.ts → ../../templates (source layout)
17032
- path3.resolve(__dirname, "..", "..", "templates"),
17033
- // src/commands/init.ts → ../../../templates (just in case dist gets nested)
17034
- path3.resolve(__dirname, "..", "..", "..", "templates")
17366
+ path5.resolve(__dirname, "..", "templates"),
17367
+ // src/utils/templates-dir.ts → ../../templates (source layout)
17368
+ path5.resolve(__dirname, "..", "..", "templates"),
17369
+ // src/utils/templates-dir.ts → ../../../templates (just in case dist gets nested)
17370
+ path5.resolve(__dirname, "..", "..", "..", "templates")
17035
17371
  ].filter((p) => p !== null);
17036
17372
  for (const candidate of candidates) {
17037
17373
  if (existsSync(candidate)) return candidate;
@@ -17042,10 +17378,402 @@ function getTemplatesDir() {
17042
17378
  Set RELYIO_CLI_TEMPLATES_DIR to point at the templates folder, or reinstall the CLI.`
17043
17379
  );
17044
17380
  }
17381
+ function schemaTemplateNames(templatesDir) {
17382
+ return readdirSync(templatesDir).filter((item) => {
17383
+ const fullPath = path5.join(templatesDir, item);
17384
+ return statSync(fullPath).isDirectory() && item !== "node_modules" && !item.startsWith(".") && existsSync(path5.join(fullPath, "schemas"));
17385
+ }).sort();
17386
+ }
17387
+
17388
+ // src/commands/init-next.ts
17389
+ init_config();
17390
+ init_manage();
17391
+ var NEXT_ENV = {
17392
+ apiKey: "RELYIO_API_KEY",
17393
+ previewApiKey: "RELYIO_PREVIEW_API_KEY",
17394
+ webhookSecret: "RELYIO_WEBHOOK_SECRET"
17395
+ };
17396
+ var REVALIDATE_PATH = "/api/relyio/revalidate";
17397
+ var REVALIDATION_EVENTS = [
17398
+ "entry.published",
17399
+ "entry.unpublished",
17400
+ "entry.deleted",
17401
+ "entry.archived",
17402
+ "entry.restored"
17403
+ ];
17404
+ var WEBHOOK_ACTION_SLUG = "http-post";
17405
+ var ENV_FILE = ".env.local";
17406
+ var FRAMEWORK_DIR = path5.join("frameworks", "next");
17407
+ var TEMPLATE_SUFFIX = ".tmpl";
17408
+ var DATASET_PLACEHOLDER = "{{DATASET}}";
17409
+ var NEXT_CONFIG = "next.config.ts";
17410
+ var NEXT_CONFIG_NAMES = [
17411
+ "next.config.ts",
17412
+ "next.config.mts",
17413
+ "next.config.js",
17414
+ "next.config.mjs",
17415
+ "next.config.cjs"
17416
+ ];
17417
+ function revalidationUrl(siteUrl) {
17418
+ let parsed;
17419
+ try {
17420
+ parsed = new URL(siteUrl);
17421
+ } catch {
17422
+ throw new Error(`Outbound URL is not a valid URL: ${siteUrl}`);
17423
+ }
17424
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
17425
+ throw new Error(
17426
+ `Outbound URL protocol "${parsed.protocol}" is not allowed (only http: and https:)`
17427
+ );
17428
+ }
17429
+ const hostname = parsed.hostname.toLowerCase();
17430
+ if (["localhost", "broadcasthost", "ip6-localhost", "ip6-loopback"].includes(hostname) || hostname.endsWith(".localhost") || hostname.endsWith(".local")) {
17431
+ throw new Error(
17432
+ `Outbound URL hostname "${hostname}" is not allowed. The webhook is sent to your deployed site; pass its public URL as --site-url.`
17433
+ );
17434
+ }
17435
+ const address = hostname.replaceAll(/^\[|\]$/g, "");
17436
+ if (isLocalAddress(address)) {
17437
+ throw new Error(
17438
+ `Outbound URL IP "${address}" is in a private/loopback range. The webhook is sent to your deployed site; pass its public URL as --site-url.`
17439
+ );
17440
+ }
17441
+ const base = `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
17442
+ return `${base}${REVALIDATE_PATH}`;
17443
+ }
17444
+ function isLocalAddress(address) {
17445
+ const v4 = /^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(address);
17446
+ if (v4) {
17447
+ const a = Number(v4[1]);
17448
+ const b = Number(v4[2]);
17449
+ return a === 0 || a === 127 || a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
17450
+ }
17451
+ if (!address.includes(":")) return false;
17452
+ return address === "::" || address === "::1" || /^f[cd][0-9a-f]{2}:/.test(address) || /^fe[89ab][0-9a-f]:/.test(address);
17453
+ }
17454
+ function assignmentOf(name) {
17455
+ return new RegExp(`^(\\s*(?:export\\s+)?)${name}(?:\\s*=|:\\s)(.*)$`);
17456
+ }
17457
+ function parseEnvValue(raw) {
17458
+ const value = raw.trim();
17459
+ const quote = value[0];
17460
+ if (quote === '"' || quote === "'" || quote === "`") {
17461
+ const end = value.indexOf(quote, 1);
17462
+ return end === -1 ? value.slice(1) : value.slice(1, end);
17463
+ }
17464
+ return (value.split("#")[0] ?? "").trim();
17465
+ }
17466
+ function readEnvValue(content, name) {
17467
+ if (content === null) return void 0;
17468
+ const pattern = assignmentOf(name);
17469
+ let value;
17470
+ for (const line of content.split(/\r?\n/)) {
17471
+ const match = pattern.exec(line);
17472
+ if (match) value = parseEnvValue(match[2] ?? "");
17473
+ }
17474
+ return value === void 0 || value === "" ? void 0 : value;
17475
+ }
17476
+ function upsertEnv(content, values) {
17477
+ const source = content ?? "";
17478
+ const eol = source.includes("\r\n") ? "\r\n" : "\n";
17479
+ const lines = source.split(/\r?\n/);
17480
+ if (lines.at(-1) === "") lines.pop();
17481
+ const written = [];
17482
+ const kept = [];
17483
+ for (const [name, value] of Object.entries(values)) {
17484
+ if (readEnvValue(source, name) !== void 0) {
17485
+ kept.push(name);
17486
+ continue;
17487
+ }
17488
+ const pattern = assignmentOf(name);
17489
+ let last = -1;
17490
+ for (const [index, line] of lines.entries()) {
17491
+ if (pattern.test(line)) last = index;
17492
+ }
17493
+ if (last === -1) {
17494
+ lines.push(`${name}=${value}`);
17495
+ } else {
17496
+ const prefix = pattern.exec(lines[last] ?? "")?.[1] ?? "";
17497
+ lines[last] = `${prefix}${name}=${value}`;
17498
+ }
17499
+ written.push(name);
17500
+ }
17501
+ if (written.length === 0) return { content: source, written, kept };
17502
+ return { content: `${lines.join(eol)}${eol}`, written, kept };
17503
+ }
17504
+ function renderNextTemplate(source, dataset) {
17505
+ const literal = dataset.replaceAll("\\", "\\\\").replaceAll("'", String.raw`\'`);
17506
+ return source.replaceAll(DATASET_PLACEHOLDER, literal);
17507
+ }
17508
+ function nextTemplateFiles(templatesDir = getTemplatesDir()) {
17509
+ const root = path5.join(templatesDir, FRAMEWORK_DIR);
17510
+ const found = [];
17511
+ const walk2 = (directory) => {
17512
+ for (const name of readdirSync(directory)) {
17513
+ const full = path5.join(directory, name);
17514
+ if (statSync(full).isDirectory()) walk2(full);
17515
+ else if (name.endsWith(TEMPLATE_SUFFIX)) found.push(path5.relative(root, full));
17516
+ }
17517
+ };
17518
+ walk2(root);
17519
+ return found.sort();
17520
+ }
17521
+ function writeNextScaffold(cwd, dataset, templatesDir = getTemplatesDir()) {
17522
+ const useSrc = existsSync(path5.join(cwd, "src", "app")) && !existsSync(path5.join(cwd, "app"));
17523
+ const files = [];
17524
+ let nextConfigToMerge;
17525
+ for (const template of nextTemplateFiles(templatesDir)) {
17526
+ const text = renderNextTemplate(
17527
+ readFileSync(path5.join(templatesDir, FRAMEWORK_DIR, template), "utf8"),
17528
+ dataset
17529
+ );
17530
+ let target = template.slice(0, -TEMPLATE_SUFFIX.length).split(path5.sep).join("/");
17531
+ if (target === NEXT_CONFIG) {
17532
+ const existing = NEXT_CONFIG_NAMES.find((name) => existsSync(path5.join(cwd, name)));
17533
+ if (existing !== void 0) {
17534
+ nextConfigToMerge = { file: existing, fragment: text };
17535
+ continue;
17536
+ }
17537
+ }
17538
+ if (useSrc && (target.startsWith("app/") || target.startsWith("lib/"))) {
17539
+ target = `src/${target}`;
17540
+ }
17541
+ const destination = path5.join(cwd, target);
17542
+ if (existsSync(destination)) {
17543
+ const current = readFileSync(destination, "utf8");
17544
+ files.push({ path: target, outcome: current === text ? "unchanged" : "kept" });
17545
+ continue;
17546
+ }
17547
+ mkdirSync(path5.dirname(destination), { recursive: true });
17548
+ writeFileSync(destination, text);
17549
+ files.push({ path: target, outcome: "written" });
17550
+ }
17551
+ return { files, nextConfigToMerge };
17552
+ }
17553
+ var defaultIo = () => ({
17554
+ cwd: process.cwd(),
17555
+ log: (line) => console.log(line),
17556
+ interactive: process.stdin.isTTY === true
17557
+ });
17558
+ function named(text, max) {
17559
+ return text.trim().slice(0, max);
17560
+ }
17561
+ async function runInitNext(options, io = defaultIo()) {
17562
+ const { cwd, log } = io;
17563
+ const projectId = options.project ?? config_default.getProjectId();
17564
+ if (projectId === void 0 || projectId === "") {
17565
+ throw new Error("No project. Run 'relyio link' in this directory, or pass --project <id>.");
17566
+ }
17567
+ const url = revalidationUrl(await requireSiteUrl(options.siteUrl, io.interactive));
17568
+ const site = new URL(url).host;
17569
+ const dataset = await resolveDataset(projectId, options.dataset, io.interactive);
17570
+ const connections = await getProjectConnections(projectId);
17571
+ const conflicting = connections.find(
17572
+ (connection) => connection.type === "webhook" && connection.config.url === url
17573
+ );
17574
+ if (conflicting !== void 0) {
17575
+ throw new Error(
17576
+ `A webhook Connection to ${url} already exists: "${conflicting.name}" (${conflicting.id}). Nothing was created. Delete it and its Automation in the dashboard, or pass another --site-url, and run again.`
17577
+ );
17578
+ }
17579
+ const envPath = path5.join(cwd, ENV_FILE);
17580
+ const envBefore = existsSync(envPath) ? readFileSync(envPath, "utf8") : null;
17581
+ let envNow = envBefore;
17582
+ const written = [];
17583
+ const kept = Object.values(NEXT_ENV).filter((name) => readEnvValue(envBefore, name) !== void 0);
17584
+ const persist = (name, value) => {
17585
+ const update = upsertEnv(envNow, { [name]: value });
17586
+ if (update.written.length === 0) return;
17587
+ writeFileSync(envPath, update.content, { mode: 384 });
17588
+ envNow = update.content;
17589
+ written.push(...update.written);
17590
+ };
17591
+ const created = [];
17592
+ try {
17593
+ let publishedKey;
17594
+ let previewKey;
17595
+ const signingSecret = readEnvValue(envBefore, NEXT_ENV.webhookSecret) ?? randomBytes(32).toString("hex");
17596
+ persist(NEXT_ENV.webhookSecret, signingSecret);
17597
+ if (readEnvValue(envBefore, NEXT_ENV.apiKey) === void 0) {
17598
+ const key = await createProjectApiKey(projectId, {
17599
+ name: named(`Next.js ${site} published`, 80),
17600
+ keyType: "production",
17601
+ readOnly: true,
17602
+ defaultDatasetId: dataset.id,
17603
+ allowedDatasetIds: [dataset.id]
17604
+ });
17605
+ publishedKey = { id: key.id, name: key.name };
17606
+ created.push(`API key "${key.name}" (${key.id})`);
17607
+ persist(NEXT_ENV.apiKey, requireToken(key.token, "published"));
17608
+ }
17609
+ if (readEnvValue(envBefore, NEXT_ENV.previewApiKey) === void 0) {
17610
+ const key = await createProjectApiKey(projectId, {
17611
+ name: named(`Next.js ${site} preview`, 80),
17612
+ keyType: "preview",
17613
+ previewScope: "drafts",
17614
+ defaultDatasetId: dataset.id,
17615
+ allowedDatasetIds: [dataset.id]
17616
+ });
17617
+ previewKey = { id: key.id, name: key.name };
17618
+ created.push(`API key "${key.name}" (${key.id})`);
17619
+ persist(NEXT_ENV.previewApiKey, requireToken(key.token, "preview"));
17620
+ }
17621
+ const connection = await createConnection(projectId, {
17622
+ name: named(`Next.js revalidation ${site}`, 100),
17623
+ type: "webhook",
17624
+ config: { url, signingSecret }
17625
+ });
17626
+ created.push(`Connection "${connection.name}" (${connection.id})`);
17627
+ const automation = await createAutomation(projectId, {
17628
+ name: named(`Revalidate ${site} on entry changes`, 100),
17629
+ events: [...REVALIDATION_EVENTS],
17630
+ connectionId: connection.id,
17631
+ actionSlug: WEBHOOK_ACTION_SLUG,
17632
+ filterDatasets: [dataset.id]
17633
+ });
17634
+ created.push(`Automation "${automation.name}" (${automation.id})`);
17635
+ const scaffold = writeNextScaffold(cwd, dataset.name);
17636
+ const result = {
17637
+ dataset: { id: dataset.id, name: dataset.name },
17638
+ publishedKey,
17639
+ previewKey,
17640
+ connection: { id: connection.id, name: connection.name, url },
17641
+ automation: { id: automation.id, name: automation.name },
17642
+ env: { written, kept },
17643
+ scaffold
17644
+ };
17645
+ report(result, log);
17646
+ return result;
17647
+ } catch (error2) {
17648
+ const message = error2 instanceof Error ? error2.message : String(error2);
17649
+ const partial = created.length === 0 ? "" : `
17650
+ Created before the failure: ${created.join("; ")}. Keys and the secret are in ${ENV_FILE}; run again to finish.`;
17651
+ throw new Error(`${message}${partial}`);
17652
+ }
17653
+ }
17654
+ function requireToken(token, kind) {
17655
+ if (typeof token !== "string" || token === "") {
17656
+ throw new Error(`manage minted the ${kind} key but served no token for it`);
17657
+ }
17658
+ return token;
17659
+ }
17660
+ async function requireSiteUrl(siteUrl, interactive) {
17661
+ if (siteUrl !== void 0) return siteUrl;
17662
+ if (!interactive) {
17663
+ throw new Error(
17664
+ "Pass --site-url <url>: your deployed site's URL, which the revalidation webhook targets."
17665
+ );
17666
+ }
17667
+ const { answer } = await inquirer2.prompt([
17668
+ {
17669
+ type: "input",
17670
+ name: "answer",
17671
+ message: "Your deployed site's URL (the revalidation webhook targets it):",
17672
+ validate: (input) => {
17673
+ try {
17674
+ revalidationUrl(input);
17675
+ return true;
17676
+ } catch (error2) {
17677
+ return error2.message;
17678
+ }
17679
+ }
17680
+ }
17681
+ ]);
17682
+ return answer;
17683
+ }
17684
+ async function resolveDataset(projectId, name, interactive) {
17685
+ if (name !== void 0) {
17686
+ const found = await getProjectDatasets(projectId, { q: name });
17687
+ const match = found.find((d) => d.name === name);
17688
+ if (match === void 0) {
17689
+ throw new Error(
17690
+ `No dataset named "${name}" in project ${projectId}. Run without --dataset to pick one.`
17691
+ );
17692
+ }
17693
+ return match;
17694
+ }
17695
+ const datasets = await getProjectDatasets(projectId);
17696
+ const names = datasets.map((d) => d.name).join(", ");
17697
+ if (!interactive) {
17698
+ throw new Error(`Pass --dataset <name>. This project's datasets: ${names}`);
17699
+ }
17700
+ const { answer } = await inquirer2.prompt([
17701
+ {
17702
+ type: "list",
17703
+ name: "answer",
17704
+ message: "Dataset the site reads:",
17705
+ choices: datasets.map((d) => ({ name: d.name, value: d.id })),
17706
+ default: datasets.find((d) => d.isDefault)?.id
17707
+ }
17708
+ ]);
17709
+ const chosen = datasets.find((d) => d.id === answer);
17710
+ if (chosen === void 0) throw new Error(`No dataset with id ${answer}`);
17711
+ return chosen;
17712
+ }
17713
+ function report(result, log) {
17714
+ const row = (label, value) => log(` ${label.padEnd(15)}${value}`);
17715
+ const kept = (name) => `kept the ${name} already in ${ENV_FILE}; none minted`;
17716
+ log(success("Provisioned RelyIO for this Next.js app"));
17717
+ row("Dataset", `${result.dataset.name} (${result.dataset.id})`);
17718
+ row(
17719
+ "Published key",
17720
+ result.publishedKey ? `${result.publishedKey.name} (${result.publishedKey.id})` : kept(NEXT_ENV.apiKey)
17721
+ );
17722
+ row(
17723
+ "Preview key",
17724
+ result.previewKey ? `${result.previewKey.name} (${result.previewKey.id})` : kept(NEXT_ENV.previewApiKey)
17725
+ );
17726
+ row(
17727
+ "Connection",
17728
+ `${result.connection.name} (${result.connection.id}) \u2192 ${result.connection.url}`
17729
+ );
17730
+ row("Automation", `${result.automation.name} (${result.automation.id})`);
17731
+ row(
17732
+ ENV_FILE,
17733
+ [
17734
+ result.env.written.length > 0 ? `wrote ${result.env.written.join(", ")}` : "",
17735
+ result.env.kept.length > 0 ? `kept ${result.env.kept.join(", ")}` : ""
17736
+ ].filter(Boolean).join("; ")
17737
+ );
17738
+ for (const file of result.scaffold.files) {
17739
+ row(file.outcome, file.outcome === "kept" ? `${file.path} (differs; left as is)` : file.path);
17740
+ }
17741
+ if (result.scaffold.nextConfigToMerge) {
17742
+ log(
17743
+ warning(
17744
+ `${result.scaffold.nextConfigToMerge.file} exists and was not edited. Merge its images block:`
17745
+ )
17746
+ );
17747
+ log(result.scaffold.nextConfigToMerge.fragment);
17748
+ }
17749
+ log(
17750
+ info(
17751
+ `Set ${Object.values(NEXT_ENV).join(", ")} in your host's environment too: the webhook reaches the deployed site, which does not read ${ENV_FILE}.`
17752
+ )
17753
+ );
17754
+ }
17755
+
17756
+ // src/commands/init.ts
17757
+ var FRAMEWORK_TEMPLATES = ["next"];
17045
17758
  async function handleInit(options) {
17759
+ if (options.template !== void 0) {
17760
+ if (options.example !== void 0) {
17761
+ throw new Error(
17762
+ "--template scaffolds a framework app and --example picks a schema template; pass one."
17763
+ );
17764
+ }
17765
+ if (options.template !== "next") {
17766
+ throw new Error(
17767
+ `Unknown --template '${options.template}'. Available: ${FRAMEWORK_TEMPLATES.join(", ")}`
17768
+ );
17769
+ }
17770
+ await requireAuth();
17771
+ await runInitNext(options);
17772
+ return;
17773
+ }
17046
17774
  console.log(heading("Initialize a new RelyIO project"));
17047
17775
  if (config_default.isProjectInitialized()) {
17048
- const { shouldReinitialize } = await inquirer.prompt([
17776
+ const { shouldReinitialize } = await inquirer2.prompt([
17049
17777
  {
17050
17778
  type: "confirm",
17051
17779
  name: "shouldReinitialize",
@@ -17079,7 +17807,7 @@ async function handleInit(options) {
17079
17807
  }
17080
17808
  }
17081
17809
  async function collectProjectInfo(options) {
17082
- const { directory } = await inquirer.prompt([
17810
+ const { directory } = await inquirer2.prompt([
17083
17811
  {
17084
17812
  type: "input",
17085
17813
  name: "directory",
@@ -17087,7 +17815,7 @@ async function collectProjectInfo(options) {
17087
17815
  default: process.cwd()
17088
17816
  }
17089
17817
  ]);
17090
- const { projectName } = await inquirer.prompt([
17818
+ const { projectName } = await inquirer2.prompt([
17091
17819
  {
17092
17820
  type: "input",
17093
17821
  name: "projectName",
@@ -17101,12 +17829,7 @@ async function collectProjectInfo(options) {
17101
17829
  console.error(error(`Templates directory not found at: ${templatesDir}`));
17102
17830
  process.exit(1);
17103
17831
  }
17104
- const dirContents = readdirSync(templatesDir);
17105
- const templates = dirContents.filter((item) => {
17106
- const fullPath = path3.join(templatesDir, item);
17107
- const stats = statSync(fullPath);
17108
- return stats.isDirectory() && item !== "node_modules" && !item.startsWith(".");
17109
- }).map((template) => {
17832
+ const templates = schemaTemplateNames(templatesDir).map((template) => {
17110
17833
  const description = (() => {
17111
17834
  switch (template) {
17112
17835
  case "bare": {
@@ -17134,7 +17857,7 @@ async function collectProjectInfo(options) {
17134
17857
  }
17135
17858
  console.log(heading("\nSelect a template for your project"));
17136
17859
  console.log(info("Use \u2191 and \u2193 arrow keys to select, then press Enter\n"));
17137
- const { selectedExample } = await inquirer.prompt([
17860
+ const { selectedExample } = await inquirer2.prompt([
17138
17861
  {
17139
17862
  type: "list",
17140
17863
  name: "selectedExample",
@@ -17159,7 +17882,7 @@ async function collectProjectInfo(options) {
17159
17882
  try {
17160
17883
  existingProjects = await getProjects();
17161
17884
  if (existingProjects.length > 0) {
17162
- const { linkType } = await inquirer.prompt([
17885
+ const { linkType } = await inquirer2.prompt([
17163
17886
  {
17164
17887
  type: "list",
17165
17888
  name: "linkType",
@@ -17173,7 +17896,7 @@ async function collectProjectInfo(options) {
17173
17896
  ]);
17174
17897
  if (linkType === "existing") {
17175
17898
  createProject = false;
17176
- const { selectedProject } = await inquirer.prompt([
17899
+ const { selectedProject } = await inquirer2.prompt([
17177
17900
  {
17178
17901
  type: "list",
17179
17902
  name: "selectedProject",
@@ -17208,12 +17931,12 @@ async function collectProjectInfo(options) {
17208
17931
  }
17209
17932
  function createProjectStructure(context) {
17210
17933
  const { directory, example = "bare" } = context;
17211
- const relyioDir = path3.resolve(directory, "relyio");
17934
+ const relyioDir = path5.resolve(directory, "relyio");
17212
17935
  if (!existsSync(relyioDir)) {
17213
17936
  mkdirSync(relyioDir, { recursive: true });
17214
17937
  }
17215
17938
  const templatesDir = getTemplatesDir();
17216
- const templateDir = path3.resolve(templatesDir, example);
17939
+ const templateDir = path5.resolve(templatesDir, example);
17217
17940
  if (!existsSync(templateDir)) {
17218
17941
  throw new Error(
17219
17942
  `Template '${example}' not found. Available templates: ${existsSync(templatesDir) ? readdirSync(templatesDir).join(", ") : "none"}`
@@ -17226,7 +17949,7 @@ function createProjectStructure(context) {
17226
17949
  if (isDirectory) {
17227
17950
  if (!existsSync(dest)) mkdirSync(dest, { recursive: true });
17228
17951
  for (const childItemName of readdirSync(src)) {
17229
- copyRecursive(path3.join(src, childItemName), path3.join(dest, childItemName));
17952
+ copyRecursive(path5.join(src, childItemName), path5.join(dest, childItemName));
17230
17953
  }
17231
17954
  } else {
17232
17955
  cpSync(src, dest);
@@ -17242,7 +17965,13 @@ function initCommand(program2) {
17242
17965
  (options) => handleInit(options),
17243
17966
  false
17244
17967
  // Don't require auth for init command
17245
- ).option("--example <type>", "Initialize with an example template (bare or blog)");
17968
+ ).option("--example <type>", "Initialize with an example template (bare or blog)").option(
17969
+ "--template <framework>",
17970
+ "Scaffold a framework app and provision its keys, webhook and env file (next)"
17971
+ ).option(
17972
+ "--site-url <url>",
17973
+ "With --template next: your deployed site's URL; the revalidation webhook targets it"
17974
+ ).option("--dataset <name>", "With --template next: the dataset the site reads").option("--project <id>", "With --template next: the project id (default: the linked project)");
17246
17975
  }
17247
17976
  init_config();
17248
17977
  async function handleLogin(options) {
@@ -17268,7 +17997,7 @@ async function handleLogin(options) {
17268
17997
  try {
17269
17998
  const user = await auth_default.getCurrentUser();
17270
17999
  console.log(warning(`You are already logged in as ${colors.highlight(user.email)}`));
17271
- const { shouldLogout } = await inquirer.prompt([
18000
+ const { shouldLogout } = await inquirer2.prompt([
17272
18001
  {
17273
18002
  type: "confirm",
17274
18003
  name: "shouldLogout",
@@ -18578,60 +19307,51 @@ async function pollTaskUntilDone(projectId, taskId, options = {}) {
18578
19307
 
18579
19308
  // src/commands/promote.ts
18580
19309
  init_config();
18581
- var DEV_CONTEXT_DIR = ".relyio";
18582
- var DEV_CONTEXT_FILE = path3.join(DEV_CONTEXT_DIR, "dev.json");
18583
- var GITIGNORE_ENTRY = `${DEV_CONTEXT_DIR}/dev.json`;
19310
+
19311
+ // src/dev/dev-context.ts
19312
+ init_dist();
19313
+ init_dev_handshake();
18584
19314
  function devContextPath(projectRoot) {
18585
- return path3.join(projectRoot, DEV_CONTEXT_FILE);
19315
+ return path5.join(projectRoot, DEV_CONTEXT_FILE);
18586
19316
  }
18587
19317
  async function writeDevContext(projectRoot, context) {
18588
- await mkdir(path3.join(projectRoot, DEV_CONTEXT_DIR), { recursive: true });
18589
- await writeFile(devContextPath(projectRoot), `${JSON.stringify(context, null, 2)}
18590
- `, "utf8");
19318
+ writeSecretFile(devContextPath(projectRoot), context);
18591
19319
  await ensureGitignore(projectRoot);
18592
19320
  }
18593
19321
  async function readDevContext(projectRoot) {
19322
+ let raw;
18594
19323
  try {
18595
- const raw = await readFile(devContextPath(projectRoot), "utf8");
18596
- const parsed = JSON.parse(raw);
18597
- if (typeof parsed.datasetId === "string" && typeof parsed.previewApiKey === "string" && typeof parsed.branchId === "string") {
18598
- return {
18599
- datasetId: parsed.datasetId,
18600
- previewApiKey: parsed.previewApiKey,
18601
- branchId: parsed.branchId,
18602
- expiresAt: typeof parsed.expiresAt === "string" ? parsed.expiresAt : null
18603
- };
18604
- }
18605
- return null;
19324
+ raw = await readFile(devContextPath(projectRoot), "utf8");
18606
19325
  } catch {
18607
- return null;
19326
+ return { state: "absent" };
18608
19327
  }
19328
+ return parseDevContextFile(raw, Date.now());
18609
19329
  }
18610
19330
  async function clearDevContext(projectRoot) {
18611
19331
  await rm(devContextPath(projectRoot), { force: true }).catch(() => {
18612
19332
  });
18613
19333
  }
18614
19334
  async function ensureGitignore(projectRoot) {
18615
- const gitignorePath = path3.join(projectRoot, ".gitignore");
19335
+ const gitignorePath = path5.join(projectRoot, ".gitignore");
18616
19336
  if (!existsSync(gitignorePath)) {
18617
19337
  await writeFile(
18618
19338
  gitignorePath,
18619
19339
  `# RelyIO sandbox dev artifact (contains a scoped preview key \u2014 never commit)
18620
- ${GITIGNORE_ENTRY}
19340
+ ${DEV_CONTEXT_FILE}
18621
19341
  `,
18622
19342
  "utf8"
18623
19343
  );
18624
19344
  return;
18625
19345
  }
18626
19346
  const current = await readFile(gitignorePath, "utf8");
18627
- const alreadyIgnored = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === GITIGNORE_ENTRY || line === `/${GITIGNORE_ENTRY}`);
19347
+ const alreadyIgnored = current.split(/\r?\n/).map((line) => line.trim()).some((line) => line === DEV_CONTEXT_FILE || line === `/${DEV_CONTEXT_FILE}`);
18628
19348
  if (alreadyIgnored) return;
18629
19349
  const prefix = current.endsWith("\n") || current.length === 0 ? "" : "\n";
18630
19350
  await writeFile(
18631
19351
  gitignorePath,
18632
19352
  `${current}${prefix}
18633
19353
  # RelyIO sandbox dev artifact (contains a scoped preview key \u2014 never commit)
18634
- ${GITIGNORE_ENTRY}
19354
+ ${DEV_CONTEXT_FILE}
18635
19355
  `,
18636
19356
  "utf8"
18637
19357
  );
@@ -18648,12 +19368,13 @@ function requireProjectId() {
18648
19368
  return projectId;
18649
19369
  }
18650
19370
  async function resolveActiveSandbox(projectId) {
19371
+ const artifact = await readDevContext(process.cwd());
18651
19372
  const sandboxes = await sandbox_default.listSandboxes(projectId);
18652
- if (sandboxes.length === 0) return null;
18653
- const devContext = await readDevContext(process.cwd());
18654
- if (devContext) {
18655
- const byArtifact = sandboxes.find((s) => s.branch.id === devContext.branchId);
18656
- if (byArtifact) return byArtifact;
19373
+ if (sandboxes.length === 0) return { sandbox: null, artifact };
19374
+ if (artifact.state === "expired") return { sandbox: null, artifact };
19375
+ if (artifact.state === "active") {
19376
+ const byArtifact = sandboxes.find((s) => s.branch.id === artifact.context.branchId);
19377
+ if (byArtifact) return { sandbox: byArtifact, artifact };
18657
19378
  }
18658
19379
  const gitBranch = getCurrentGitBranch();
18659
19380
  if (gitBranch) {
@@ -18662,12 +19383,25 @@ async function resolveActiveSandbox(projectId) {
18662
19383
  const parent = branches.find((b) => b.name === gitBranch);
18663
19384
  if (parent) {
18664
19385
  const byParent = sandboxes.find((s) => s.branch.parentBranchId === parent.id);
18665
- if (byParent) return byParent;
19386
+ if (byParent) return { sandbox: byParent, artifact };
18666
19387
  }
18667
19388
  } catch {
18668
19389
  }
18669
19390
  }
18670
- return sandboxes.length === 1 ? sandboxes[0] : null;
19391
+ return { sandbox: sandboxes.length === 1 ? sandboxes[0] : null, artifact };
19392
+ }
19393
+ function reportArtifact(artifact) {
19394
+ if (artifact.state === "expired") {
19395
+ displayWarning(
19396
+ `Your sandbox's local dev artifact expired at ${artifact.expiredAt}.`,
19397
+ "It can no longer identify which sandbox to promote, and guessing from your git branch could promote a different one. Run `relyio dev` to refresh it, then promote."
19398
+ );
19399
+ } else if (artifact.state === "unreadable") {
19400
+ displayWarning(
19401
+ `Your \`.relyio/dev.json\` could not be read (${artifact.reason}).`,
19402
+ "Your app is not reading the sandbox. Delete the file and run `relyio dev` to rewrite it."
19403
+ );
19404
+ }
18671
19405
  }
18672
19406
  async function resolveTargetBranch(projectId, requested, defaultParentBranchId) {
18673
19407
  const branches = await manage_default.getProjectBranches(projectId);
@@ -18688,14 +19422,16 @@ async function handlePromote(options) {
18688
19422
  displayHeader("RelyIO Promote", "Flush your sandbox into a real branch");
18689
19423
  const projectId = requireProjectId();
18690
19424
  const spinner2 = createSpinner("Resolving your active sandbox...");
18691
- const sandbox = await resolveActiveSandbox(projectId);
19425
+ const { sandbox, artifact } = await resolveActiveSandbox(projectId);
18692
19426
  if (!sandbox) {
18693
19427
  spinner2.fail("No active sandbox found");
19428
+ reportArtifact(artifact);
18694
19429
  displayInfo("Start one with `relyio dev`, then run `relyio promote` again.");
18695
19430
  process.exit(1);
18696
19431
  return;
18697
19432
  }
18698
19433
  spinner2.succeed(`Sandbox: ${sandbox.branch.name}`);
19434
+ reportArtifact(artifact);
18699
19435
  const target = await resolveTargetBranch(projectId, options.branch, sandbox.branch.parentBranchId);
18700
19436
  if (!target) {
18701
19437
  displayError(
@@ -18810,7 +19546,8 @@ async function offerPromoteWhenSandboxActive() {
18810
19546
  if (!projectId) return false;
18811
19547
  let sandbox;
18812
19548
  try {
18813
- sandbox = await resolveActiveSandbox(projectId);
19549
+ ;
19550
+ ({ sandbox } = await resolveActiveSandbox(projectId));
18814
19551
  } catch {
18815
19552
  return false;
18816
19553
  }
@@ -18983,7 +19720,7 @@ async function handleDeploy(options) {
18983
19720
  let notes;
18984
19721
  if (!json && !options.yes) {
18985
19722
  console.log();
18986
- const { confirmed } = await inquirer.prompt([
19723
+ const { confirmed } = await inquirer2.prompt([
18987
19724
  {
18988
19725
  type: "confirm",
18989
19726
  name: "confirmed",
@@ -18996,7 +19733,7 @@ async function handleDeploy(options) {
18996
19733
  return;
18997
19734
  }
18998
19735
  if (!options.skipNotes) {
18999
- const answer = await inquirer.prompt([
19736
+ const answer = await inquirer2.prompt([
19000
19737
  {
19001
19738
  type: "input",
19002
19739
  name: "notes",
@@ -19703,7 +20440,7 @@ function sandboxCommands(program2) {
19703
20440
  }
19704
20441
 
19705
20442
  // src/commands/version.ts
19706
- var CLI_VERSION = "0.2.2" ;
20443
+ var CLI_VERSION = "0.3.1" ;
19707
20444
  async function handleVersion(_options) {
19708
20445
  console.log(heading("RelyIO CLI Version Information"));
19709
20446
  const versionInfo = [
@@ -19786,13 +20523,13 @@ var TerminalWatcher = class extends EventEmitter {
19786
20523
  ignoreInitial: true,
19787
20524
  persistent: true
19788
20525
  });
19789
- this.watcher.on("raw", (event, path23) => {
20526
+ this.watcher.on("raw", (event, path25) => {
19790
20527
  if (event === "change" || event === "rename") {
19791
- return this.handleFileChange(path23);
20528
+ return this.handleFileChange(path25);
19792
20529
  }
19793
20530
  });
19794
- this.watcher.on("add", (path23) => this.handleFileChange(path23));
19795
- this.watcher.on("unlink", (path23) => this.handleFileChange(path23));
20531
+ this.watcher.on("add", (path25) => this.handleFileChange(path25));
20532
+ this.watcher.on("unlink", (path25) => this.handleFileChange(path25));
19796
20533
  this.watcher.on("error", (error2) => {
19797
20534
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
19798
20535
  if (this.options.onError && error2 instanceof Error) {
@@ -20476,7 +21213,7 @@ async function handleLink(options) {
20476
21213
  console.log(info("Please create an organization from the dashboard."));
20477
21214
  process.exit(1);
20478
21215
  }
20479
- const { organizationId } = await inquirer.prompt([
21216
+ const { organizationId } = await inquirer2.prompt([
20480
21217
  {
20481
21218
  type: "list",
20482
21219
  name: "organizationId",
@@ -20496,7 +21233,7 @@ async function handleLink(options) {
20496
21233
  console.log(info("Please create a project first."));
20497
21234
  process.exit(1);
20498
21235
  }
20499
- const { projectId } = await inquirer.prompt([
21236
+ const { projectId } = await inquirer2.prompt([
20500
21237
  {
20501
21238
  type: "list",
20502
21239
  name: "projectId",
@@ -20546,172 +21283,13 @@ init_schema_loader();
20546
21283
  init_schema();
20547
21284
  init_manage();
20548
21285
  init_manage_error();
20549
- var BUILT_IN_LANGUAGES = ["typescript", "python"];
20550
- var registeredPlugins = /* @__PURE__ */ new Map();
20551
- function registerPlugin(language, load3) {
20552
- registeredPlugins.set(language, load3);
20553
- }
20554
- function asPlugin(module_, describe3) {
20555
- const candidate = module_?.default ?? module_;
20556
- const plugin3 = candidate;
20557
- if (!plugin3 || typeof plugin3.generate !== "function") {
20558
- throw new Error(`Invalid plugin: ${describe3}`);
20559
- }
20560
- return plugin3;
20561
- }
20562
- async function loadPlugin(language) {
20563
- const registered = registeredPlugins.get(language);
20564
- if (registered) {
20565
- return asPlugin(await registered(), `registered ${language}`);
20566
- }
20567
- if (language === "typescript" || language === "python") {
20568
- try {
20569
- const pluginName2 = `@relyio/codegen-${language}`;
20570
- const pluginModule = await import(
20571
- /* webpackIgnore: true */
20572
- pluginName2
20573
- );
20574
- const plugin3 = pluginModule.default || pluginModule;
20575
- if (!plugin3 || typeof plugin3.generate !== "function") {
20576
- throw new Error(`Invalid built-in plugin: ${language}`);
20577
- }
20578
- return plugin3;
20579
- } catch {
20580
- try {
20581
- const relativePath = path3.resolve(
20582
- path3.dirname(import.meta.url.replace("file://", "")),
20583
- `../../codegen-${language}/dist/index.js`
20584
- );
20585
- const pluginModule = await import(
20586
- /* webpackIgnore: true */
20587
- relativePath
20588
- );
20589
- const plugin3 = pluginModule.default || pluginModule;
20590
- if (!plugin3 || typeof plugin3.generate !== "function") {
20591
- throw new Error(`Invalid built-in plugin: ${language}`);
20592
- }
20593
- return plugin3;
20594
- } catch {
20595
- throw new Error(
20596
- `Built-in ${language} generator not found. Please ensure @relyio/codegen-${language} is built.`
20597
- );
20598
- }
20599
- }
20600
- }
20601
- const pluginName = `@relyio/codegen-${language}`;
20602
- try {
20603
- const pluginModule = await import(
20604
- /* webpackIgnore: true */
20605
- pluginName
20606
- );
20607
- const plugin3 = pluginModule.default || pluginModule;
20608
- if (!plugin3 || typeof plugin3.generate !== "function") {
20609
- throw new Error(`Invalid plugin: ${pluginName}`);
20610
- }
20611
- return plugin3;
20612
- } catch (error2) {
20613
- const code = error2 !== null && typeof error2 === "object" && "code" in error2 ? String(error2.code) : void 0;
20614
- if (code === "MODULE_NOT_FOUND") {
20615
- throw new Error(`Generator "${language}" not installed.
20616
- Run: pnpm add -D ${pluginName}`);
20617
- }
20618
- throw error2;
20619
- }
20620
- }
20621
- async function getAvailablePlugins() {
20622
- try {
20623
- const packageJsonPath = path3.join(process.cwd(), "package.json");
20624
- const packageJsonModule = await import(packageJsonPath);
20625
- const packageJson = packageJsonModule.default;
20626
- const dependencies = {
20627
- ...packageJson.dependencies,
20628
- ...packageJson.devDependencies
20629
- };
20630
- const plugins = Object.keys(dependencies).filter((name) => name.startsWith("@relyio/codegen-")).map((name) => name.replace("@relyio/codegen-", ""));
20631
- if (!plugins.includes("typescript")) {
20632
- plugins.unshift("typescript");
20633
- }
20634
- if (!plugins.includes("python")) {
20635
- plugins.unshift("python");
20636
- }
20637
- return plugins;
20638
- } catch {
20639
- return ["typescript", "python"];
20640
- }
20641
- }
20642
- function getSuggestedPlugins() {
20643
- return {
20644
- python: "@relyio/codegen-python",
20645
- go: "@relyio/codegen-go",
20646
- rust: "@relyio/codegen-rust",
20647
- java: "@relyio/codegen-java",
20648
- csharp: "@relyio/codegen-csharp",
20649
- php: "@relyio/codegen-php",
20650
- swift: "@relyio/codegen-swift",
20651
- kotlin: "@relyio/codegen-kotlin"
20652
- };
20653
- }
20654
- async function writeFiles(files, outputDir) {
20655
- await fs4.mkdir(outputDir, { recursive: true });
20656
- await Promise.all(
20657
- files.map(async (file) => {
20658
- const fullPath = path3.join(outputDir, file.path);
20659
- const dir = path3.dirname(fullPath);
20660
- await fs4.mkdir(dir, { recursive: true });
20661
- await fs4.writeFile(fullPath, file.content, "utf8");
20662
- })
20663
- );
20664
- }
20665
- var TemplateEngine = class {
20666
- handlebars;
20667
- constructor() {
20668
- this.handlebars = Handlebars.create();
20669
- this.registerHelpers();
20670
- }
20671
- registerHelpers() {
20672
- this.handlebars.registerHelper("pascalCase", (str) => {
20673
- if (!str) return "";
20674
- return str.replaceAll(/(^|_)(\w)/g, (_, __, char) => char.toUpperCase());
20675
- });
20676
- this.handlebars.registerHelper("camelCase", (str) => {
20677
- if (!str) return "";
20678
- const pascal = str.replaceAll(/(^|_)(\w)/g, (_, __, char) => char.toUpperCase());
20679
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
20680
- });
20681
- this.handlebars.registerHelper("snakeCase", (str) => {
20682
- if (!str) return "";
20683
- return str.replaceAll(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
20684
- });
20685
- this.handlebars.registerHelper("kebabCase", (str) => {
20686
- if (!str) return "";
20687
- return str.replaceAll(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
20688
- });
20689
- this.handlebars.registerHelper("plural", (str) => {
20690
- if (!str) return "";
20691
- if (str.endsWith("y")) {
20692
- return str.slice(0, -1) + "ies";
20693
- }
20694
- if (str.endsWith("s") || str.endsWith("sh") || str.endsWith("ch")) {
20695
- return str + "es";
20696
- }
20697
- return str + "s";
20698
- });
20699
- }
20700
- compile(template) {
20701
- const compiledTemplate = this.handlebars.compile(template);
20702
- return (context) => compiledTemplate(context);
20703
- }
20704
- render(template, context) {
20705
- const compiledTemplate = this.compile(template);
20706
- return compiledTemplate(context);
20707
- }
20708
- };
20709
- new TemplateEngine();
21286
+ init_dist2();
20710
21287
 
20711
21288
  // src/codegen/built-in-generators.ts
21289
+ init_dist2();
20712
21290
  var BUILT_IN_GENERATOR_LOADERS = Object.freeze({
20713
- typescript: () => Promise.resolve().then(() => (init_dist2(), dist_exports)),
20714
- python: () => Promise.resolve().then(() => (init_dist3(), dist_exports2))
21291
+ typescript: () => Promise.resolve().then(() => (init_dist4(), dist_exports)),
21292
+ python: () => Promise.resolve().then(() => (init_dist5(), dist_exports2))
20715
21293
  });
20716
21294
  function registerBuiltInGenerators() {
20717
21295
  for (const language of BUILT_IN_LANGUAGES) {
@@ -20722,6 +21300,9 @@ registerBuiltInGenerators();
20722
21300
 
20723
21301
  // src/commands/generate-types.ts
20724
21302
  init_git();
21303
+
21304
+ // src/utils/codegen-config.ts
21305
+ init_dist2();
20725
21306
  var CodegenConfigSchema = z.object({
20726
21307
  output: z.string().optional(),
20727
21308
  languages: z.array(z.string()).optional(),
@@ -20736,16 +21317,11 @@ var CodegenConfigSchema = z.object({
20736
21317
  output: z.string().optional(),
20737
21318
  baseModel: z.enum(["pydantic", "dataclass"]).optional(),
20738
21319
  includeValidators: z.boolean().optional()
20739
- }).optional(),
20740
- go: z.object({
20741
- output: z.string().optional(),
20742
- packageName: z.string().optional(),
20743
- includeValidators: z.boolean().optional()
20744
21320
  }).optional()
20745
21321
  });
20746
21322
  async function loadCodegenConfig(configPath) {
20747
- const defaultPath = path3.join(process.cwd(), "relyio.codegen.yaml");
20748
- const alternatePath = path3.join(process.cwd(), "relyio.codegen.yml");
21323
+ const defaultPath = path5.join(process.cwd(), "relyio.codegen.yaml");
21324
+ const alternatePath = path5.join(process.cwd(), "relyio.codegen.yml");
20749
21325
  let targetPath = configPath;
20750
21326
  if (!targetPath) {
20751
21327
  try {
@@ -20789,12 +21365,11 @@ function mergeConfigWithOptions(config, options, language) {
20789
21365
  moduleFormat: options.moduleFormat || languageConfig.moduleFormat || "esm"
20790
21366
  };
20791
21367
  }
20792
- function validateLanguages(languages) {
20793
- const supportedLanguages = ["typescript", "python", "go", "rust", "csharp", "java"];
21368
+ function assertSupportedLanguages(languages) {
20794
21369
  for (const language of languages) {
20795
- if (!supportedLanguages.includes(language)) {
21370
+ if (!BUILT_IN_LANGUAGES.includes(language)) {
20796
21371
  throw new Error(
20797
- `Unsupported language: ${language}. Supported languages: ${supportedLanguages.join(", ")}`
21372
+ `Unsupported language: ${language}. Supported languages: ${BUILT_IN_LANGUAGES.join(", ")}`
20798
21373
  );
20799
21374
  }
20800
21375
  }
@@ -20810,24 +21385,8 @@ function formatSchemaSource3(result) {
20810
21385
  const count = files.length;
20811
21386
  return `YAML (${count} file${count === 1 ? "" : "s"})`;
20812
21387
  }
20813
- async function handleGenerationError(error2) {
21388
+ function handleGenerationError(error2) {
20814
21389
  if (error2 instanceof Error) {
20815
- if (error2.message.includes("not installed")) {
20816
- const availablePlugins = await getAvailablePlugins();
20817
- const suggestedPlugins = getSuggestedPlugins();
20818
- displayInfo("\u{1F4CB} Available generators:");
20819
- for (const plugin3 of availablePlugins) {
20820
- const status = plugin3 === "typescript" || plugin3 === "python" ? " (built-in)" : "";
20821
- displayInfo(` \u2705 ${plugin3}${status}`);
20822
- }
20823
- displayInfo("\n\u{1F4BE} Install additional generators with:");
20824
- for (const [language, packageName] of Object.entries(suggestedPlugins)) {
20825
- if (!availablePlugins.includes(language)) {
20826
- displayInfo(` pnpm add -D ${packageName}`);
20827
- }
20828
- }
20829
- return;
20830
- }
20831
21390
  if (error2.message.includes("Schema validation failed") || error2.message.includes("validation error")) {
20832
21391
  displayInfo("\n\u{1F4A1} Troubleshooting steps:");
20833
21392
  displayInfo("1. Run 'relyio status' to see validation details");
@@ -20896,14 +21455,16 @@ async function performTypeGeneration(branchName, options, watchMode = false) {
20896
21455
  `Failed to load configuration: ${error2 instanceof Error ? error2.message : "Unknown error"}`
20897
21456
  );
20898
21457
  }
20899
- if (options.languages || options.language) {
20900
- const requestedLanguages = options.languages ? options.languages.split(",") : [options.language || "typescript"];
20901
- ctx.languages = [...new Set(requestedLanguages)];
20902
- } else {
20903
- const configLanguages = ctx.config.languages || ["typescript"];
20904
- ctx.languages = [...new Set(configLanguages)];
20905
- }
20906
- validateLanguages(ctx.languages);
21458
+ const requested = options.languages || options.language ? [
21459
+ ...new Set(
21460
+ options.languages ? options.languages.split(",") : [options.language || "typescript"]
21461
+ )
21462
+ ] : (
21463
+ // Use config file languages if no CLI language specified
21464
+ [...new Set(ctx.config.languages || ["typescript"])]
21465
+ );
21466
+ assertSupportedLanguages(requested);
21467
+ ctx.languages = requested;
20907
21468
  }
20908
21469
  },
20909
21470
  {
@@ -21041,7 +21602,7 @@ async function performTypeGeneration(branchName, options, watchMode = false) {
21041
21602
  "Type generation failed",
21042
21603
  error2 instanceof Error ? error2.message : "Unknown error"
21043
21604
  );
21044
- await handleGenerationError(error2);
21605
+ handleGenerationError(error2);
21045
21606
  process.exit(1);
21046
21607
  }
21047
21608
  }
@@ -21111,18 +21672,17 @@ function generateTypesCommand(program2) {
21111
21672
  "after",
21112
21673
  `
21113
21674
  Examples:
21114
- $ relyio generate-types # Generate TypeScript types
21115
- $ relyio generate-types -o ./src/types # Custom output directory
21116
- $ relyio generate-types --watch # Watch mode
21117
- $ relyio generate-types -l python # Generate Python types
21118
- $ relyio generate-types --languages ts,py # Multiple languages
21675
+ $ relyio generate-types # Generate TypeScript types
21676
+ $ relyio generate-types -o ./src/types # Custom output directory
21677
+ $ relyio generate-types --watch # Watch mode
21678
+ $ relyio generate-types -l python # Generate Python types
21679
+ $ relyio generate-types --languages typescript,python # Multiple languages
21119
21680
 
21120
- Supported Languages:
21121
- typescript (ts) TypeScript type definitions
21122
- python (py) Python type hints (dataclasses)
21681
+ Supported languages:
21682
+ ${BUILT_IN_LANGUAGES.join(", ")}
21123
21683
 
21124
21684
  Configuration:
21125
- Create codegen.config.ts or codegen.config.json for advanced options.
21685
+ Create relyio.codegen.yaml (or .yml) for advanced options.
21126
21686
  `
21127
21687
  );
21128
21688
  }
@@ -21299,7 +21859,6 @@ function displayObjectDiff(obj, indent, mode) {
21299
21859
  const color = THEME3.added ;
21300
21860
  const prefix = "+" ;
21301
21861
  for (const [key, value] of Object.entries(obj)) {
21302
- if (key === "_tag") continue;
21303
21862
  if (Array.isArray(value)) {
21304
21863
  console.log(color(`${indent}${prefix} ${key}:`));
21305
21864
  for (const item of value) {
@@ -21796,40 +22355,23 @@ var THEME5 = {
21796
22355
  link: chalk12.cyan.underline
21797
22356
  };
21798
22357
  async function getDashboardPath(target) {
21799
- const dashboardUrl = config_default.getDashboardUrl();
21800
- const projectId = config_default.getProjectId();
21801
- const organizationId = config_default.getOrganizationId();
22358
+ const dashboardUrl = config_default.getDashboardUrl().replace(/\/+$/, "");
21802
22359
  switch (target) {
21803
22360
  case "dashboard": {
21804
22361
  return dashboardUrl;
21805
22362
  }
21806
- case "project": {
21807
- if (!projectId) {
21808
- throw new Error("No project linked. Run 'relyio link' first.");
21809
- }
21810
- return `${dashboardUrl}/projects/${projectId}`;
21811
- }
22363
+ // The Dashboard has no page per branch, so a branch opens the Project it belongs to.
22364
+ case "project":
21812
22365
  case "branch": {
21813
- if (!projectId) {
21814
- throw new Error("No project linked. Run 'relyio link' first.");
21815
- }
21816
- const currentBranch = getCurrentGitBranch2();
21817
- if (currentBranch) {
21818
- try {
21819
- const branches = await manage_default.getProjectBranches(projectId, { name: currentBranch });
21820
- if (branches.length > 0) {
21821
- return `${dashboardUrl}/projects/${projectId}/branches/${branches[0].id}`;
21822
- }
21823
- } catch {
21824
- }
21825
- }
21826
- return `${dashboardUrl}/projects/${projectId}`;
22366
+ const { organizationId, projectId } = await linkedProject();
22367
+ return `${dashboardUrl}/o/${encodeURIComponent(organizationId)}/${encodeURIComponent(projectId)}`;
21827
22368
  }
21828
22369
  case "settings": {
22370
+ const organizationId = config_default.getOrganizationId();
21829
22371
  if (organizationId) {
21830
- return `${dashboardUrl}/organizations/${organizationId}/settings`;
22372
+ return `${dashboardUrl}/o/${encodeURIComponent(organizationId)}/settings`;
21831
22373
  }
21832
- return `${dashboardUrl}/settings`;
22374
+ return `${dashboardUrl}/account/settings`;
21833
22375
  }
21834
22376
  case "docs": {
21835
22377
  return "https://docs.relyio.com";
@@ -21842,12 +22384,15 @@ async function getDashboardPath(target) {
21842
22384
  }
21843
22385
  }
21844
22386
  }
21845
- function getCurrentGitBranch2() {
21846
- try {
21847
- return execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf8" }).trim();
21848
- } catch {
21849
- return null;
22387
+ async function linkedProject() {
22388
+ const projectId = config_default.getProjectId();
22389
+ if (!projectId) {
22390
+ throw new Error("No project linked. Run 'relyio link' first.");
21850
22391
  }
22392
+ const linked = config_default.getOrganizationId();
22393
+ if (linked) return { organizationId: linked, projectId };
22394
+ const project = await manage_default.getProject(projectId);
22395
+ return { organizationId: project.organizationId, projectId };
21851
22396
  }
21852
22397
  async function handleOpen(target, options) {
21853
22398
  const resolvedTarget = target || options.target || "dashboard";
@@ -21862,6 +22407,9 @@ async function handleOpen(target, options) {
21862
22407
  console.log();
21863
22408
  console.log(THEME5.success("\u2713") + ` Opening ${THEME5.bold(resolvedTarget)} in browser...`);
21864
22409
  console.log(THEME5.muted(` ${url}`));
22410
+ if (resolvedTarget === "branch") {
22411
+ console.log(THEME5.muted(" The Dashboard has no page per branch, so this is the project."));
22412
+ }
21865
22413
  await open(url);
21866
22414
  } catch (error2) {
21867
22415
  console.error(chalk12.red(`Failed to open: ${error2.message}`));
@@ -21882,7 +22430,7 @@ function openCommand(program2) {
21882
22430
  Targets:
21883
22431
  dashboard Open the main dashboard (default)
21884
22432
  project Open the current project
21885
- branch Open the current branch
22433
+ branch Open the current branch's project (the Dashboard has no page per branch)
21886
22434
  settings Open settings page
21887
22435
  docs Open documentation
21888
22436
  api Open API documentation
@@ -22287,7 +22835,7 @@ var THEME7 = {
22287
22835
  alias: chalk12.cyan,
22288
22836
  command: chalk12.yellow
22289
22837
  };
22290
- var ALIAS_FILE = path3.join(os.homedir(), ".relyio", "aliases.json");
22838
+ var ALIAS_FILE = path5.join(os.homedir(), ".relyio", "aliases.json");
22291
22839
  function readAliases() {
22292
22840
  try {
22293
22841
  if (fs2.existsSync(ALIAS_FILE)) {
@@ -22298,7 +22846,7 @@ function readAliases() {
22298
22846
  return {};
22299
22847
  }
22300
22848
  function writeAliases(aliases) {
22301
- const dir = path3.dirname(ALIAS_FILE);
22849
+ const dir = path5.dirname(ALIAS_FILE);
22302
22850
  if (!fs2.existsSync(dir)) {
22303
22851
  fs2.mkdirSync(dir, { recursive: true });
22304
22852
  }
@@ -22438,6 +22986,7 @@ function runPreflight() {
22438
22986
  }
22439
22987
 
22440
22988
  // src/dev/cockpit.tsx
22989
+ init_dev_handshake();
22441
22990
  init_app();
22442
22991
  init_dev_cockpit_context();
22443
22992
  var WrappedChildProcess = class {
@@ -22452,8 +23001,7 @@ var WrappedChildProcess = class {
22452
23001
  cwd: this.options.cwd,
22453
23002
  env: {
22454
23003
  ...process.env,
22455
- RELYIO_DEV_DATASET: this.options.env.RELYIO_DEV_DATASET,
22456
- RELYIO_DEV_API_KEY: this.options.env.RELYIO_DEV_API_KEY
23004
+ ...this.options.env
22457
23005
  },
22458
23006
  stdio: ["ignore", "pipe", "pipe"]
22459
23007
  });
@@ -22678,10 +23226,14 @@ function DevCockpit({
22678
23226
  command: childCommand.command,
22679
23227
  args: childCommand.args,
22680
23228
  cwd: projectRoot,
22681
- env: {
22682
- RELYIO_DEV_DATASET: sandbox.datasetId,
22683
- RELYIO_DEV_API_KEY: sandbox.previewApiKey
22684
- },
23229
+ // The wrapped child and the standalone `.relyio/dev.json` reader must see the same
23230
+ // sandbox, deadline included — before REL-647 the env carrier had no deadline at all, so a
23231
+ // lapsed sandbox kept working here for the life of the process.
23232
+ env: buildDevEnv({
23233
+ datasetId: sandbox.datasetId,
23234
+ previewApiKey: sandbox.previewApiKey,
23235
+ expiresAt: sandbox.expiresAt
23236
+ }),
22685
23237
  onOutput: (chunk) => setChildLines((previous) => appendChildLines(previous, chunk)),
22686
23238
  onExit: () => setChildRunning(false)
22687
23239
  });
@@ -22935,8 +23487,8 @@ function getAuthToken7() {
22935
23487
  throw new Error("Not authenticated. Run `relyio login` first.");
22936
23488
  }
22937
23489
  var ConversationsClient = class {
22938
- async request(method, path23, init = {}) {
22939
- const url = new URL(`${getGatewayUrl()}${AI_GATEWAY_CONVERSATIONS_PATH}${path23}`);
23490
+ async request(method, path25, init = {}) {
23491
+ const url = new URL(`${getGatewayUrl()}${AI_GATEWAY_CONVERSATIONS_PATH}${path25}`);
22940
23492
  if (init.query) {
22941
23493
  for (const [k, v] of Object.entries(init.query)) {
22942
23494
  if (v === void 0) continue;
@@ -22955,7 +23507,7 @@ var ConversationsClient = class {
22955
23507
  const raw = await response.text().catch(() => "");
22956
23508
  const refusal = readError(parseJson(raw));
22957
23509
  throw new Error(
22958
- refusal === void 0 ? `Conversations API ${method} ${path23} \u2192 ${response.status}: ${raw}` : `Conversations API ${method} ${path23} \u2192 ${response.status} ${refusal.code}: ${refusal.messages.join(" ")}`
23510
+ refusal === void 0 ? `Conversations API ${method} ${path25} \u2192 ${response.status}: ${raw}` : `Conversations API ${method} ${path25} \u2192 ${response.status} ${refusal.code}: ${refusal.messages.join(" ")}`
22959
23511
  );
22960
23512
  }
22961
23513
  const text = await response.text();
@@ -23205,10 +23757,13 @@ async function resolveModelFlag(rawRef) {
23205
23757
  return "invalid";
23206
23758
  }
23207
23759
  if (!match.available) {
23208
- const reason = match.disabledReason === "paused" ? "AI is paused globally" : "this model is currently disabled by the workspace admin";
23209
- console.log(chalk12.red(`
23210
- Cannot use ${match.name} \u2014 ${reason}.
23211
- `));
23760
+ console.log(
23761
+ chalk12.red(
23762
+ `
23763
+ Cannot use ${match.name} \u2014 this model is currently disabled by the workspace admin.
23764
+ `
23765
+ )
23766
+ );
23212
23767
  return "invalid";
23213
23768
  }
23214
23769
  return match.id;
@@ -24037,7 +24592,7 @@ Examples:
24037
24592
  }
24038
24593
 
24039
24594
  // src/index.ts
24040
- var CLI_VERSION3 = "0.2.2" ;
24595
+ var CLI_VERSION3 = "0.3.1" ;
24041
24596
  var program = new Command();
24042
24597
  program.name("relyio").description("RelyIO CLI - Content Management, Simplified").version(CLI_VERSION3);
24043
24598
  registerGlobalOptions(program);