@gravity-rail/cli 0.22.0 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -244,13 +244,19 @@ cat member.json | gr members update -w $WORKSPACE --id 42 --allow-write
244
244
  | `3` | A human-in-the-loop (HITL) interrupt requires out-of-band action before the operation can continue |
245
245
  | `4` | No assistant reply — a `--message` turn made tool calls but returned no final summary, or an inference error occurred with no final message (side effects may have been applied) |
246
246
 
247
- **UI choices** (`present_choice_list` / `render_ui`) are handled in the terminal
248
- when stdin is a TTY: the CLI parses OpenUI markup into an arrow-key picker
249
- (ChoiceButtons) or field-by-field form prompts, then resumes the chat with your
250
- selection. Manager (`gr manage`) and Concierge (`gr concierge chat`) share this
251
- path. When stdin is not a TTY, those interrupts resume with an empty value (the
252
- agent handles the missing response). Unsupported HITL types still exit `3` with
253
- the JSON contract below.
247
+ **UI prompts** (`present_choice_list` / `render_ui`, and the `connect_*_account`
248
+ login prompts) do not pause the agent. The tool returns straight away and the
249
+ prompt arrives as a tool result, which the CLI draws in the terminal when stdin
250
+ is a TTY: OpenUI markup becomes an arrow-key picker (ChoiceButtons, with Space
251
+ to tick a multi-select) or field-by-field form prompts, and a connect prompt
252
+ prints the link to open. Your answer is then sent as an ordinary chat message,
253
+ so the agent picks it up on its next turn. Manager (`gr manage`) and Concierge
254
+ (`gr concierge chat`) share this path. Typing prose instead of using the
255
+ controls sends exactly what you typed. When stdin is not a TTY the prompt is
256
+ printed and left unanswered, and the turn ends normally.
257
+
258
+ Approvals, access grants and checkout still **interrupt** the run and resume it
259
+ in place; unsupported interrupt types exit `3` with the JSON contract below.
254
260
 
255
261
  When a concierge command run with `--message` (non-interactive mode) hits a HITL
256
262
  interrupt the CLI cannot resolve on its own, it exits `3` and writes a single
@@ -439,6 +445,19 @@ gr workspaces get -w $SOURCE_WORKSPACE -o json | \
439
445
  gr workspaces create --data "$(cat -)" --allow-write
440
446
  ```
441
447
 
448
+ ### Check configuration readiness
449
+
450
+ ```sh
451
+ gr workspaces configuration-readiness -w $WORKSPACE -o json
452
+ ```
453
+
454
+ The combined read fails closed in its `configurationReady` field when
455
+ configuration-health collectors fail, critical or warning findings remain,
456
+ post-import bindings are missing, skipped EventRules remain outstanding, or a
457
+ readiness scan is truncated. Informational configuration findings are reported
458
+ but do not block. This is a saved-configuration check, not an end-to-end launch
459
+ or patient-experience certification.
460
+
442
461
  ### List all workflows with their task counts
443
462
 
444
463
  ```sh
@@ -2,8 +2,8 @@ import {
2
2
  confirmAccountPhoneVerify,
3
3
  requireAuth,
4
4
  startAccountPhoneVerify
5
- } from "./chunk-EGO3LMNZ.js";
6
- import "./chunk-IYCL6P6B.js";
5
+ } from "./chunk-KHUZAYUS.js";
6
+ import "./chunk-IGZVMTG5.js";
7
7
  import "./chunk-3T7A5PSF.js";
8
8
 
9
9
  // src/commands/account-verify-phone.ts
@@ -38,4 +38,4 @@ async function promptLine(question) {
38
38
  export {
39
39
  accountVerifyPhoneCommand
40
40
  };
41
- //# sourceMappingURL=account-verify-phone-QJDC57CB.js.map
41
+ //# sourceMappingURL=account-verify-phone-5DY6ETMV.js.map
@@ -1,5 +1,6 @@
1
1
  // src/cli-utils/config.ts
2
- import { existsSync, readFileSync } from "fs";
2
+ import { existsSync, readFileSync, statSync } from "fs";
3
+ import { CORE_SCHEMA, defineSequenceTag, load } from "js-yaml";
3
4
  import { dirname, join, resolve } from "path";
4
5
  var ENVIRONMENTS = {
5
6
  dev: {
@@ -18,46 +19,174 @@ var ENVIRONMENTS = {
18
19
  var CALLBACK_PORT = 31337;
19
20
  var CALLBACK_PATH = "/oauth/callback";
20
21
  var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
21
- function detectWorktreePorts() {
22
- let dir = resolve(process.cwd());
23
- let overridePath;
22
+ var API_CONTAINER_PORT = 443;
23
+ var FRONTEND_CONTAINER_PORT = 3e3;
24
+ var WORKTREE_SETUP_MARKER = "worktree-setup.sh";
25
+ var OVERRIDE_FILENAME = "docker-compose.override.yml";
26
+ var OVERRIDE_SEQUENCE_TAG = defineSequenceTag("!override", {
27
+ create: () => [],
28
+ addItem: (carrier, item) => {
29
+ carrier.push(item);
30
+ },
31
+ identify: () => false
32
+ });
33
+ var COMPOSE_OVERRIDE_SCHEMA = CORE_SCHEMA.withTags(OVERRIDE_SEQUENCE_TAG);
34
+ var WorktreeConfigError = class extends Error {
35
+ constructor(message, checkoutRoot, overridePath) {
36
+ super(message);
37
+ this.checkoutRoot = checkoutRoot;
38
+ this.overridePath = overridePath;
39
+ this.name = "WorktreeConfigError";
40
+ }
41
+ checkoutRoot;
42
+ overridePath;
43
+ };
44
+ function isRecord(value) {
45
+ return typeof value === "object" && value !== null && !Array.isArray(value);
46
+ }
47
+ function parsePortToken(token) {
48
+ if (token === void 0 || !/^\d+$/.test(token)) return void 0;
49
+ const value = Number(token);
50
+ if (!Number.isInteger(value) || value <= 0 || value > 65535) return void 0;
51
+ return value;
52
+ }
53
+ function parsePublishedTarget(mapping) {
54
+ const parts = mapping.split(":");
55
+ if (parts.length < 2) return void 0;
56
+ const target = parsePortToken(parts[parts.length - 1]);
57
+ const published = parsePortToken(parts[parts.length - 2]);
58
+ if (target === void 0 || published === void 0) return void 0;
59
+ return { published, target };
60
+ }
61
+ function publishedPortForTarget(ports, target) {
62
+ if (!Array.isArray(ports)) return void 0;
63
+ for (const entry of ports) {
64
+ if (typeof entry !== "string") continue;
65
+ const parsed = parsePublishedTarget(entry);
66
+ if (parsed?.target === target) return parsed.published;
67
+ }
68
+ return void 0;
69
+ }
70
+ function servicePorts(services, name) {
71
+ const service = services[name];
72
+ if (!isRecord(service)) return void 0;
73
+ return service["ports"];
74
+ }
75
+ function findNearestGit(startDir) {
76
+ let dir = resolve(startDir);
24
77
  while (true) {
25
- const candidate = join(dir, "docker-compose.override.yml");
26
- if (existsSync(candidate)) {
27
- overridePath = candidate;
28
- break;
78
+ const gitPath = join(dir, ".git");
79
+ if (existsSync(gitPath)) {
80
+ try {
81
+ const st = statSync(gitPath);
82
+ return { root: dir, gitIsFile: st.isFile() };
83
+ } catch {
84
+ }
29
85
  }
30
86
  const parent = dirname(dir);
31
- if (parent === dir) break;
87
+ if (parent === dir) return void 0;
32
88
  dir = parent;
33
89
  }
34
- if (!overridePath) return {};
90
+ }
91
+ function unresolved(root, overridePath, reason) {
92
+ return { kind: "unresolved-worktree", root, overridePath, reason };
93
+ }
94
+ function loadComposeDocument(content) {
95
+ let loaded;
96
+ try {
97
+ loaded = load(content, { schema: COMPOSE_OVERRIDE_SCHEMA });
98
+ } catch {
99
+ return void 0;
100
+ }
101
+ return isRecord(loaded) ? loaded : void 0;
102
+ }
103
+ function classifyCheckout(startDir = process.cwd()) {
104
+ const git = findNearestGit(startDir);
105
+ if (!git || !git.gitIsFile) {
106
+ return { kind: "non-worktree", root: git?.root };
107
+ }
108
+ const overridePath = join(git.root, OVERRIDE_FILENAME);
109
+ if (!existsSync(overridePath)) {
110
+ return unresolved(
111
+ git.root,
112
+ overridePath,
113
+ `docker-compose.override.yml (generated by ${WORKTREE_SETUP_MARKER})`
114
+ );
115
+ }
35
116
  let content;
36
117
  try {
37
118
  content = readFileSync(overridePath, "utf-8");
38
119
  } catch {
39
- return {};
40
- }
41
- if (!content.includes("worktree-setup.sh")) return {};
42
- let apiPort;
43
- let frontendPort;
44
- for (const match of content.matchAll(/-\s*'(\d+):(\d+)'/g)) {
45
- const host = parseInt(match[1], 10);
46
- const container = parseInt(match[2], 10);
47
- if (container === 443 || container === 5054) apiPort = host;
48
- if (container === 3e3) frontendPort = host;
49
- }
50
- return { apiPort, frontendPort };
120
+ return unresolved(git.root, overridePath, "readable generated worktree override");
121
+ }
122
+ if (!content.includes(WORKTREE_SETUP_MARKER)) {
123
+ return unresolved(
124
+ git.root,
125
+ overridePath,
126
+ `generated ${WORKTREE_SETUP_MARKER} override marker`
127
+ );
128
+ }
129
+ const document = loadComposeDocument(content);
130
+ if (!document) {
131
+ return unresolved(git.root, overridePath, "valid generated worktree override YAML");
132
+ }
133
+ const services = document["services"];
134
+ if (!isRecord(services)) {
135
+ return unresolved(
136
+ git.root,
137
+ overridePath,
138
+ `api-nginx published port for container ${API_CONTAINER_PORT}; frontend published port for container ${FRONTEND_CONTAINER_PORT}`
139
+ );
140
+ }
141
+ const apiPort = publishedPortForTarget(servicePorts(services, "api-nginx"), API_CONTAINER_PORT);
142
+ const frontendPort = publishedPortForTarget(
143
+ servicePorts(services, "frontend"),
144
+ FRONTEND_CONTAINER_PORT
145
+ );
146
+ const missing = [];
147
+ if (apiPort === void 0) {
148
+ missing.push(`api-nginx published port for container ${API_CONTAINER_PORT}`);
149
+ }
150
+ if (frontendPort === void 0) {
151
+ missing.push(`frontend published port for container ${FRONTEND_CONTAINER_PORT}`);
152
+ }
153
+ if (apiPort === void 0 || frontendPort === void 0) {
154
+ return unresolved(git.root, overridePath, missing.join("; "));
155
+ }
156
+ return {
157
+ kind: "resolved-worktree",
158
+ root: git.root,
159
+ overridePath,
160
+ apiPort,
161
+ frontendPort
162
+ };
163
+ }
164
+ function worktreeFailClosed(checkout, missingFields) {
165
+ throw new WorktreeConfigError(
166
+ `Cannot resolve ${missingFields.join(" and ")} URL(s) for this linked worktree.
167
+ Checkout: ${checkout.root}
168
+ Override: ${checkout.overridePath}
169
+ Missing: ${checkout.reason}
170
+ Re-run scripts/worktree-setup.sh . from this checkout, or pass explicit --api-url and GRAVITY_RAIL_FRONTEND_URL (or both GRAVITY_RAIL_API_URL and GRAVITY_RAIL_FRONTEND_URL) so neither URL inherits the primary checkout.`,
171
+ checkout.root,
172
+ checkout.overridePath
173
+ );
174
+ }
175
+ function worktreeDevUrls(apiPort, frontendPort) {
176
+ return {
177
+ apiUrl: `https://api.gravityrail.test:${apiPort}`,
178
+ frontendUrl: `https://app.gravityrail.test:${frontendPort}`
179
+ };
51
180
  }
52
- function getEnvironmentConfig(env) {
181
+ function getEnvironmentConfig(env, options = {}) {
53
182
  if (env.startsWith("dev-cloud:")) {
54
183
  const namespace = env.slice("dev-cloud:".length);
55
184
  if (!namespace) {
56
185
  throw new Error("dev-cloud environment requires a namespace: --env dev-cloud:NAMESPACE");
57
186
  }
58
187
  return {
59
- apiUrl: `https://${namespace}-api.gravityrail-dev.com`,
60
- frontendUrl: `https://${namespace}-app.gravityrail-dev.com`
188
+ apiUrl: options.apiUrl || process.env.GRAVITY_RAIL_API_URL || `https://${namespace}-api.gravityrail-dev.com`,
189
+ frontendUrl: options.frontendUrl || process.env.GRAVITY_RAIL_FRONTEND_URL || `https://${namespace}-app.gravityrail-dev.com`
61
190
  };
62
191
  }
63
192
  const config = ENVIRONMENTS[env];
@@ -66,21 +195,35 @@ function getEnvironmentConfig(env) {
66
195
  `Invalid environment: ${env}. Use 'dev', 'staging', 'prod', or 'dev-cloud:NAMESPACE'.`
67
196
  );
68
197
  }
69
- const apiUrl = process.env.GRAVITY_RAIL_API_URL || config.apiUrl;
70
- const frontendUrl = process.env.GRAVITY_RAIL_FRONTEND_URL || config.frontendUrl;
71
- if (apiUrl !== config.apiUrl || frontendUrl !== config.frontendUrl) {
72
- return { apiUrl, frontendUrl };
198
+ const explicitApi = options.apiUrl || process.env.GRAVITY_RAIL_API_URL;
199
+ const explicitFrontend = options.frontendUrl || process.env.GRAVITY_RAIL_FRONTEND_URL;
200
+ if (env !== "dev") {
201
+ return {
202
+ apiUrl: explicitApi || config.apiUrl,
203
+ frontendUrl: explicitFrontend || config.frontendUrl
204
+ };
73
205
  }
74
- if (env === "dev") {
75
- const ports = detectWorktreePorts();
76
- if (ports.apiPort || ports.frontendPort) {
77
- return {
78
- apiUrl: ports.apiPort ? `https://api.gravityrail.test:${ports.apiPort}` : config.apiUrl,
79
- frontendUrl: ports.frontendPort ? `https://app.gravityrail.test:${ports.frontendPort}` : config.frontendUrl
80
- };
81
- }
206
+ const checkout = classifyCheckout(options.startDir ?? process.cwd());
207
+ if (checkout.kind === "non-worktree") {
208
+ return {
209
+ apiUrl: explicitApi || config.apiUrl,
210
+ frontendUrl: explicitFrontend || config.frontendUrl
211
+ };
82
212
  }
83
- return config;
213
+ if (checkout.kind === "resolved-worktree") {
214
+ const detected = worktreeDevUrls(checkout.apiPort, checkout.frontendPort);
215
+ return {
216
+ apiUrl: explicitApi || detected.apiUrl,
217
+ frontendUrl: explicitFrontend || detected.frontendUrl
218
+ };
219
+ }
220
+ const missingFields = [];
221
+ if (!explicitApi) missingFields.push("API");
222
+ if (!explicitFrontend) missingFields.push("frontend");
223
+ if (missingFields.length > 0) {
224
+ worktreeFailClosed(checkout, missingFields);
225
+ }
226
+ return { apiUrl: explicitApi, frontendUrl: explicitFrontend };
84
227
  }
85
228
 
86
229
  // src/cli-utils/auth.ts
@@ -1008,6 +1151,33 @@ var AGUIClient = class {
1008
1151
  this.model = "model" in options ? options.model : "manager";
1009
1152
  this.appContext = options.appContext;
1010
1153
  }
1154
+ chatRouteUrl(suffix) {
1155
+ const noChat = typeof this.chatId === "number" ? this.chatId <= 0 : this.chatId.length === 0;
1156
+ if (!this.workspaceUuid || noChat) return null;
1157
+ return `${this.baseUrl}/api/v2/w/${this.workspaceUuid}/chats/${this.chatId}/${suffix}`;
1158
+ }
1159
+ /**
1160
+ * The operator's own live inline-connect link for this chat, or null when
1161
+ * none is live. The `inline_connect` prompt carries no URL on purpose (it is
1162
+ * a bearer, and the tool result is persisted and model-visible), so the
1163
+ * terminal fetches it here when the operator chooses to connect.
1164
+ */
1165
+ async fetchInlineConnectLink(appSlug) {
1166
+ const url = this.chatRouteUrl(`inline-connect/${encodeURIComponent(appSlug)}/link`);
1167
+ if (!url) return null;
1168
+ try {
1169
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${this.accessToken}` } });
1170
+ if (!res.ok) return null;
1171
+ const body = await res.json();
1172
+ if (typeof body.connectUrl !== "string") return null;
1173
+ return {
1174
+ connectUrl: body.connectUrl,
1175
+ expiresInMinutes: typeof body.expiresInMinutes === "number" ? body.expiresInMinutes : 0
1176
+ };
1177
+ } catch {
1178
+ return null;
1179
+ }
1180
+ }
1011
1181
  inferenceStatusUrl() {
1012
1182
  const noChat = typeof this.chatId === "number" ? this.chatId <= 0 : this.chatId.length === 0;
1013
1183
  if (!this.workspaceUuid || noChat) return null;
@@ -1248,6 +1418,9 @@ export {
1248
1418
  CALLBACK_PORT,
1249
1419
  CALLBACK_PATH,
1250
1420
  REDIRECT_URI,
1421
+ WorktreeConfigError,
1422
+ parsePublishedTarget,
1423
+ classifyCheckout,
1251
1424
  getEnvironmentConfig,
1252
1425
  DEFAULT_PROFILE,
1253
1426
  CredentialsManager,
@@ -1272,4 +1445,4 @@ export {
1272
1445
  getTextDelta,
1273
1446
  getErrorMessage
1274
1447
  };
1275
- //# sourceMappingURL=chunk-IYCL6P6B.js.map
1448
+ //# sourceMappingURL=chunk-IGZVMTG5.js.map