railcode 0.1.22 → 0.1.24

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
@@ -16,6 +16,9 @@ import { QueryCmdError, paramSignature, parseParamDecls, parseRunParams, pickAut
16
16
  import { APP_MANIFEST_NAME, ManifestParseError, documentOperations, parseManifestYaml, renderManifestDeployOutcome, renderManifestSummary, } from "./manifest.js";
17
17
  import { ConnectorError, buildConnectorRequest, connectorOpenapiOutput, connectorTable, parseConnectorOption, parseMethodOption, pickConnectorBody, proxyExitCode, renderConnectorDocs, renderProxyResponse, } from "./connectors.js";
18
18
  import { llmModelTable, llmProviderTable } from "./llm.js";
19
+ import { OrgAdminError, appTable, parseAccessMode, parseAssignableRole, parseGrantSubject, parseMemberList, parseResourceIds, parseResourceType, grantSubjectLabel, grantTable, memberTable, renderAppAccess, renderEffective, resolveAppRef, resolveMemberRef, resolveRoleRef, roleTable, } from "./orgadmin.js";
20
+ import { ObservabilityError, buildLogQuery, logTable, parseLimit, parseRange, renderAnalytics, resolveLogStream, validateLogSource, validateLogStatus, } from "./observability.js";
21
+ import { ConnectionAdminError, buildConnectionCreateBody, buildServiceConnectorCreateBody, connectionTable, nativeConnectorTable, parseAuthType, parseConnectionEngine, parseJsonObject, parseMethodList, resolveConnectionRef, resolveServiceConnectorRef, serviceConnectorAdminTable, } from "./connections.js";
19
22
  // ---------------------------------------------------------------------------
20
23
  // Constants
21
24
  // ---------------------------------------------------------------------------
@@ -40,11 +43,20 @@ Usage:
40
43
  railcode design-system Print your org's design-system guidance (markdown)
41
44
  railcode db <list|query> ... List data connectors / run read-only SQL
42
45
  railcode query <list|run|create|update|delete> ... Saved queries: invoke by name / author (admin)
43
- railcode connector <list|docs|fetch> ... List connectors / show API docs / proxy a call
46
+ railcode connector <list|docs|fetch|create|native|enable|delete> ...
47
+ Service connectors: call them, or manage them (admin)
48
+ railcode connections <list|create|delete> ... Manage org data connectors (admin)
44
49
  railcode llm <providers|models> List the LLM providers/models apps can call
45
50
  railcode manifest <validate|show> ... Validate ${APP_MANIFEST_NAME} locally / show an app's ratified manifest
46
51
  railcode agent <list|show|create|update|delete|run|schedule> ...
47
52
  Manage org-scoped managed agents
53
+ railcode members <list|set-role|remove|add> ... Org members + system roles (list: any; mutations: admin)
54
+ railcode roles <list|create|update|delete|add-member|remove-member|grants|grant|revoke|materialize|effective|catalog> ...
55
+ Manage org roles + the granular grants table (admin)
56
+ railcode apps <list|show|access|set-access|transfer|delete> ... Manage apps + access policy
57
+ railcode analytics <app> [--range 1d|7d|30d|90d] Per-app pageview analytics (admin/owner)
58
+ railcode logs <connector|service-connector|llm|email|agent> [filters]
59
+ Read org observability logs (admin)
48
60
  railcode --version
49
61
  railcode --help
50
62
 
@@ -93,9 +105,32 @@ async function main() {
93
105
  case "connectors":
94
106
  await commandConnector(args);
95
107
  return;
108
+ case "connection":
109
+ case "connections":
110
+ await commandConnections(args);
111
+ return;
96
112
  case "llm":
97
113
  await commandLlm(args);
98
114
  return;
115
+ case "member":
116
+ case "members":
117
+ await commandMembers(args);
118
+ return;
119
+ case "role":
120
+ case "roles":
121
+ await commandRoles(args);
122
+ return;
123
+ case "app":
124
+ case "apps":
125
+ await commandApps(args);
126
+ return;
127
+ case "analytics":
128
+ await commandAnalytics(args);
129
+ return;
130
+ case "logs":
131
+ case "log":
132
+ await commandLogs(args);
133
+ return;
99
134
  case "manifest":
100
135
  await commandManifest(args);
101
136
  return;
@@ -4700,9 +4735,27 @@ Usage:
4700
4735
  railcode connector fetch <path> --connector <name> [opts]
4701
4736
  Proxy one HTTP call through a connector
4702
4737
 
4738
+ Admin (service-connector:manage):
4739
+ railcode connector native List native presets + enabled state
4740
+ railcode connector enable <key> --credentials '{...}'
4741
+ One-click enable/rotate a native preset
4742
+ railcode connector create --name <n> --base-url <url> [--auth-type <t>]
4743
+ [--auth-config '{...}'] [--methods GET,POST] [--description <d>]
4744
+ [--content-type <type>] [--disabled]
4745
+ Create/replace a custom connector
4746
+ railcode connector delete <name|uuid> Delete a service connector
4747
+
4703
4748
  Aliases: connector = connectors; list = ls, connectors; docs = doc; fetch = request.
4704
4749
 
4750
+ Create options:
4751
+ --content-type <type> Content-Type sent with request bodies
4752
+ (default application/json; e.g.
4753
+ application/x-www-form-urlencoded for a form API)
4754
+
4705
4755
  List options:
4756
+ --admin Manage view: base_url, credential + enabled
4757
+ state, content type, native key
4758
+ (service-connector:manage)
4706
4759
  --json Print the raw connector array
4707
4760
 
4708
4761
  Docs options:
@@ -4737,6 +4790,19 @@ async function commandConnector(args) {
4737
4790
  case "request":
4738
4791
  await commandConnectorFetch(args);
4739
4792
  return;
4793
+ case "native":
4794
+ await commandConnectorNative(args);
4795
+ return;
4796
+ case "enable":
4797
+ await commandConnectorEnable(args);
4798
+ return;
4799
+ case "create":
4800
+ await commandConnectorCreate(args);
4801
+ return;
4802
+ case "delete":
4803
+ case "rm":
4804
+ await commandConnectorDelete(args);
4805
+ return;
4740
4806
  case "":
4741
4807
  case "help":
4742
4808
  console.log(CONNECTOR_HELP);
@@ -4747,17 +4813,100 @@ async function commandConnector(args) {
4747
4813
  }
4748
4814
  catch (error) {
4749
4815
  // Validation helpers throw ConnectorError; surface them as usage errors (exit 2).
4750
- if (error instanceof ConnectorError)
4816
+ if (error instanceof ConnectorError || error instanceof ConnectionAdminError) {
4751
4817
  throw new CliError(error.message, 2);
4818
+ }
4752
4819
  throw error;
4753
4820
  }
4754
4821
  }
4755
- async function commandConnectorList(args) {
4822
+ // Admin list of service connectors (with uuids, for delete/update) — distinct from
4823
+ // the member-facing `connector list`, which reads the caller-scoped data plane.
4824
+ async function fetchServiceConnectorsAdmin(config) {
4825
+ return (await authedJson(config, `/api/organizations/${config.orgUuid}/service-connectors`, "List service connectors"));
4826
+ }
4827
+ async function commandConnectorNative(args) {
4756
4828
  assertKnownConnectorOptions(args, ["json", "apiUrl"]);
4829
+ const config = requireLogin(args);
4830
+ const natives = (await authedJson(config, `/api/organizations/${config.orgUuid}/service-connectors/native`, "List native presets"));
4831
+ if (args.options.json) {
4832
+ console.log(JSON.stringify(natives, null, 2));
4833
+ return;
4834
+ }
4835
+ console.log(nativeConnectorTable(natives));
4836
+ }
4837
+ async function commandConnectorEnable(args) {
4838
+ assertKnownConnectorOptions(args, ["credentials", "credentialsFile", "apiUrl"]);
4839
+ const key = args.rest[1];
4840
+ if (!key) {
4841
+ throw new CliError("Usage: railcode connector enable <key> --credentials '{...}'. See `railcode connector native`.", 2);
4842
+ }
4843
+ const credentials = readJsonObjectOption(args, "credentials", "credentialsFile", "--credentials");
4844
+ const config = requireLogin(args);
4845
+ const created = (await authedJson(config, `/api/organizations/${config.orgUuid}/service-connectors/native/${encodeURIComponent(key)}/enable`, `Enable native connector "${key}"`, jsonBody({ credentials })));
4846
+ console.log(`Native connector "${created.name}" enabled.`);
4847
+ }
4848
+ async function commandConnectorCreate(args) {
4849
+ assertKnownConnectorOptions(args, [
4850
+ "name",
4851
+ "baseUrl",
4852
+ "authType",
4853
+ "authConfig",
4854
+ "authConfigFile",
4855
+ "description",
4856
+ "contentType",
4857
+ "methods",
4858
+ "disabled",
4859
+ "apiUrl",
4860
+ ]);
4861
+ const authType = parseAuthType(optionString(args, "authType"));
4862
+ const authConfig = authType === "none"
4863
+ ? {}
4864
+ : readJsonObjectOption(args, "authConfig", "authConfigFile", "--auth-config");
4865
+ const body = buildServiceConnectorCreateBody({
4866
+ name: optionString(args, "name"),
4867
+ baseUrl: optionString(args, "baseUrl"),
4868
+ authType,
4869
+ authConfig,
4870
+ description: optionString(args, "description"),
4871
+ contentType: optionString(args, "contentType"),
4872
+ allowedMethods: parseMethodList(optionString(args, "methods")),
4873
+ enabled: args.options.disabled !== true,
4874
+ });
4875
+ const config = requireLogin(args);
4876
+ const created = (await authedJson(config, `/api/organizations/${config.orgUuid}/service-connectors`, "Create service connector", jsonBody(body)));
4877
+ console.log(`Service connector "${created.name}" → ${created.base_url}.`);
4878
+ }
4879
+ async function commandConnectorDelete(args) {
4880
+ assertKnownConnectorOptions(args, ["apiUrl"]);
4881
+ const ref = args.rest[1];
4882
+ if (!ref)
4883
+ throw new CliError("Usage: railcode connector delete <name|uuid>", 2);
4884
+ const config = requireLogin(args);
4885
+ const conn = resolveServiceConnectorRef(await fetchServiceConnectorsAdmin(config), ref);
4886
+ await authedNoContent(config, `/api/organizations/${config.orgUuid}/service-connectors/${conn.uuid}`, `Delete service connector "${conn.name}"`, { method: "DELETE" });
4887
+ console.log(`Service connector "${conn.name}" deleted.`);
4888
+ }
4889
+ async function commandConnectorList(args) {
4890
+ assertKnownConnectorOptions(args, ["json", "admin", "apiUrl"]);
4757
4891
  if (args.rest.length > 1) {
4758
4892
  throw new CliError(`railcode connector list takes no arguments (got "${args.rest[1]}").`, 2);
4759
4893
  }
4760
4894
  const config = requireLogin(args);
4895
+ // --admin: the manage view (base_url, credential + enabled state, native key),
4896
+ // gated by service-connector:manage. Default is the caller-facing list.
4897
+ if (args.options.admin === true) {
4898
+ const connectors = await fetchServiceConnectorsAdmin(config);
4899
+ if (args.options.json) {
4900
+ console.log(JSON.stringify(connectors, null, 2));
4901
+ return;
4902
+ }
4903
+ if (connectors.length === 0) {
4904
+ console.log("No service connectors configured for this organization.");
4905
+ return;
4906
+ }
4907
+ console.log(serviceConnectorAdminTable(connectors));
4908
+ return;
4909
+ }
4761
4910
  const connectors = await fetchServiceConnectors(config);
4762
4911
  if (args.options.json) {
4763
4912
  console.log(JSON.stringify(connectors, null, 2));
@@ -4954,6 +5103,859 @@ function assertKnownLlmOptions(args, allowed) {
4954
5103
  }
4955
5104
  }
4956
5105
  // ---------------------------------------------------------------------------
5106
+ // Shared admin helpers (members / roles / apps / analytics / logs / connections)
5107
+ //
5108
+ // All of these hit the org-scoped `/api/organizations/{org}/...` management APIs
5109
+ // with the personal token. They mirror the web console's admin surfaces so an
5110
+ // operator can do everything from the CLI. Most are owner/admin-gated server-side
5111
+ // (403 for plain members); a couple (apps list/show/access of your own apps) work
5112
+ // for any member per the app access policy.
5113
+ // ---------------------------------------------------------------------------
5114
+ function rejectUnknownOptions(args, allowed, cmd, help) {
5115
+ for (const key of Object.keys(args.options)) {
5116
+ if (!allowed.includes(key)) {
5117
+ throw new CliError(`Unknown option --${camelToKebab(key)} for \`railcode ${cmd}\`.\n\n${help}`, 2);
5118
+ }
5119
+ }
5120
+ }
5121
+ // GET/POST/PATCH that returns a parsed JSON body (drops a rejected token first).
5122
+ async function authedJson(config, path, action, init = {}) {
5123
+ const resp = await authedFetch(config, path, init);
5124
+ if (resp.status === 401)
5125
+ await reLoginOrThrow();
5126
+ return readJsonOrThrow(resp, action);
5127
+ }
5128
+ // A 204/no-body mutation (DELETE, membership add/remove). Surfaces the server
5129
+ // detail on any non-2xx.
5130
+ async function authedNoContent(config, path, action, init = {}) {
5131
+ const resp = await authedFetch(config, path, init);
5132
+ if (resp.status === 401)
5133
+ await reLoginOrThrow();
5134
+ if (!resp.ok) {
5135
+ throw new CliError(`${action} failed (${resp.status}): ${await errorDetail(resp)}`, 1);
5136
+ }
5137
+ }
5138
+ function jsonBody(body) {
5139
+ return {
5140
+ method: "POST",
5141
+ headers: { "Content-Type": "application/json" },
5142
+ body: JSON.stringify(body),
5143
+ };
5144
+ }
5145
+ async function fetchMembers(config) {
5146
+ return (await authedJson(config, `/api/organizations/${config.orgUuid}/members`, "List members"));
5147
+ }
5148
+ async function fetchApps(config) {
5149
+ return (await authedJson(config, `/api/organizations/${config.orgUuid}/apps`, "List apps"));
5150
+ }
5151
+ async function fetchPermissions(config) {
5152
+ return (await authedJson(config, `/api/organizations/${config.orgUuid}/permissions`, "List roles + grants"));
5153
+ }
5154
+ // Resolve a subject spec (org | role:x | user:x) to the API's {subject_type,
5155
+ // subject_uuid} shape, resolving names/emails against the org's roles + members.
5156
+ async function resolveGrantSubject(config, spec) {
5157
+ if (spec.kind === "org")
5158
+ return { subject_type: "org", subject_uuid: null };
5159
+ if (spec.kind === "role") {
5160
+ const { roles } = await fetchPermissions(config);
5161
+ return { subject_type: "role", subject_uuid: resolveRoleRef(roles, spec.ref).uuid };
5162
+ }
5163
+ const members = await fetchMembers(config);
5164
+ return { subject_type: "user", subject_uuid: resolveMemberRef(members, spec.ref).uuid };
5165
+ }
5166
+ // ---------------------------------------------------------------------------
5167
+ // members — org members + system roles (`railcode members`)
5168
+ // ---------------------------------------------------------------------------
5169
+ const MEMBERS_HELP = `railcode members — org members + system roles
5170
+
5171
+ Usage:
5172
+ railcode members list List everyone in your org (any member)
5173
+ railcode members set-role <email|uuid> --role admin|member
5174
+ Change a member's system role (admin)
5175
+ railcode members remove <email|uuid> Detach a member from the org (admin)
5176
+ railcode members add --email <e> --name <n> --password <p> --role admin|member
5177
+ [--no-password-change] Provision a member (admin; self-hosted only)
5178
+
5179
+ list is available to any member; set-role/remove/add require admin.
5180
+
5181
+ Options:
5182
+ --json Print raw JSON (list)
5183
+ `;
5184
+ async function commandMembers(args) {
5185
+ try {
5186
+ const sub = args.rest[0] ?? "";
5187
+ switch (sub) {
5188
+ case "list":
5189
+ case "ls":
5190
+ await commandMembersList(args);
5191
+ return;
5192
+ case "set-role":
5193
+ await commandMembersSetRole(args);
5194
+ return;
5195
+ case "remove":
5196
+ case "rm":
5197
+ await commandMembersRemove(args);
5198
+ return;
5199
+ case "add":
5200
+ case "provision":
5201
+ await commandMembersAdd(args);
5202
+ return;
5203
+ case "":
5204
+ case "help":
5205
+ console.log(MEMBERS_HELP);
5206
+ return;
5207
+ default:
5208
+ throw new CliError(`Unknown members command: ${sub}\n\n${MEMBERS_HELP}`, 2);
5209
+ }
5210
+ }
5211
+ catch (error) {
5212
+ if (error instanceof OrgAdminError)
5213
+ throw new CliError(error.message, 2);
5214
+ throw error;
5215
+ }
5216
+ }
5217
+ async function commandMembersList(args) {
5218
+ rejectUnknownOptions(args, ["json", "apiUrl"], "members", MEMBERS_HELP);
5219
+ const config = requireLogin(args);
5220
+ const members = await fetchMembers(config);
5221
+ if (args.options.json) {
5222
+ console.log(JSON.stringify(members, null, 2));
5223
+ return;
5224
+ }
5225
+ if (members.length === 0) {
5226
+ console.log("No members (that shouldn't happen).");
5227
+ return;
5228
+ }
5229
+ console.log(memberTable(members));
5230
+ }
5231
+ async function commandMembersSetRole(args) {
5232
+ rejectUnknownOptions(args, ["role", "apiUrl"], "members", MEMBERS_HELP);
5233
+ const ref = args.rest[1];
5234
+ if (!ref)
5235
+ throw new CliError("Usage: railcode members set-role <email|uuid> --role admin|member", 2);
5236
+ const role = parseAssignableRole(optionString(args, "role"));
5237
+ const config = requireLogin(args);
5238
+ const member = resolveMemberRef(await fetchMembers(config), ref);
5239
+ const updated = (await authedJson(config, `/api/organizations/${config.orgUuid}/members/${member.uuid}`, "Update member role", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role }) }));
5240
+ console.log(`${updated.email} is now ${updated.role}.`);
5241
+ }
5242
+ async function commandMembersRemove(args) {
5243
+ rejectUnknownOptions(args, ["apiUrl"], "members", MEMBERS_HELP);
5244
+ const ref = args.rest[1];
5245
+ if (!ref)
5246
+ throw new CliError("Usage: railcode members remove <email|uuid>", 2);
5247
+ const config = requireLogin(args);
5248
+ const member = resolveMemberRef(await fetchMembers(config), ref);
5249
+ await authedNoContent(config, `/api/organizations/${config.orgUuid}/members/${member.uuid}`, `Remove member ${member.email}`, { method: "DELETE" });
5250
+ console.log(`Removed ${member.email} from the organization.`);
5251
+ }
5252
+ async function commandMembersAdd(args) {
5253
+ rejectUnknownOptions(args, ["email", "name", "password", "role", "noPasswordChange", "apiUrl"], "members", MEMBERS_HELP);
5254
+ const email = optionString(args, "email");
5255
+ const name = optionString(args, "name");
5256
+ const password = optionString(args, "password");
5257
+ const roleOpt = optionString(args, "role");
5258
+ if (!email || !name || !password || !roleOpt) {
5259
+ throw new CliError("railcode members add requires --email, --name, --password and --role.\n\n" + MEMBERS_HELP, 2);
5260
+ }
5261
+ const role = parseAssignableRole(roleOpt);
5262
+ const body = { email, name, password, role };
5263
+ if (args.options.noPasswordChange === true)
5264
+ body.must_change_password = false;
5265
+ const config = requireLogin(args);
5266
+ const created = (await authedJson(config, `/api/organizations/${config.orgUuid}/members`, "Provision member", jsonBody(body)));
5267
+ console.log(`Provisioned ${created.email} (${created.role}).`);
5268
+ }
5269
+ // ---------------------------------------------------------------------------
5270
+ // roles — org roles + the granular grants table (`railcode roles`)
5271
+ // ---------------------------------------------------------------------------
5272
+ const ROLES_HELP = `railcode roles — org roles + the granular grants table (admin)
5273
+
5274
+ Roles:
5275
+ railcode roles list List org roles (with member counts)
5276
+ railcode roles create --name <n> [--description <d>]
5277
+ railcode roles update <role> [--name <n>] [--description <d>]
5278
+ railcode roles delete <role>
5279
+ railcode roles add-member <role> <email|uuid>
5280
+ railcode roles remove-member <role> <email|uuid>
5281
+
5282
+ Grants (additive permissions):
5283
+ railcode roles grants List every grant row
5284
+ railcode roles grant --subject <org|role:X|user:X> --resource <type> --ids <a,b>
5285
+ railcode roles revoke <grant_id>
5286
+ railcode roles materialize --subject <...> --resource <type>
5287
+ Expand a "*" wildcard into explicit rows
5288
+ railcode roles effective <email|uuid> A member's computed effective access
5289
+ railcode roles catalog Grantable resources (apps/connectors/…)
5290
+
5291
+ <role> is a role name or UUID. --resource ∈ app|llm|email|sc_endpoint|connector|saved_query|agent.
5292
+
5293
+ Options:
5294
+ --json Print raw JSON (list/grants/catalog)
5295
+ `;
5296
+ async function commandRoles(args) {
5297
+ try {
5298
+ const sub = args.rest[0] ?? "";
5299
+ switch (sub) {
5300
+ case "list":
5301
+ case "ls":
5302
+ await commandRolesList(args);
5303
+ return;
5304
+ case "create":
5305
+ await commandRolesCreate(args);
5306
+ return;
5307
+ case "update":
5308
+ await commandRolesUpdate(args);
5309
+ return;
5310
+ case "delete":
5311
+ case "rm":
5312
+ await commandRolesDelete(args);
5313
+ return;
5314
+ case "add-member":
5315
+ await commandRolesMember(args, true);
5316
+ return;
5317
+ case "remove-member":
5318
+ await commandRolesMember(args, false);
5319
+ return;
5320
+ case "grants":
5321
+ await commandRolesGrants(args);
5322
+ return;
5323
+ case "grant":
5324
+ await commandRolesGrant(args);
5325
+ return;
5326
+ case "revoke":
5327
+ await commandRolesRevoke(args);
5328
+ return;
5329
+ case "materialize":
5330
+ await commandRolesMaterialize(args);
5331
+ return;
5332
+ case "effective":
5333
+ await commandRolesEffective(args);
5334
+ return;
5335
+ case "catalog":
5336
+ await commandRolesCatalog(args);
5337
+ return;
5338
+ case "":
5339
+ case "help":
5340
+ console.log(ROLES_HELP);
5341
+ return;
5342
+ default:
5343
+ throw new CliError(`Unknown roles command: ${sub}\n\n${ROLES_HELP}`, 2);
5344
+ }
5345
+ }
5346
+ catch (error) {
5347
+ if (error instanceof OrgAdminError)
5348
+ throw new CliError(error.message, 2);
5349
+ throw error;
5350
+ }
5351
+ }
5352
+ async function commandRolesList(args) {
5353
+ rejectUnknownOptions(args, ["json", "apiUrl"], "roles", ROLES_HELP);
5354
+ const config = requireLogin(args);
5355
+ const { roles } = await fetchPermissions(config);
5356
+ if (args.options.json) {
5357
+ console.log(JSON.stringify(roles, null, 2));
5358
+ return;
5359
+ }
5360
+ if (roles.length === 0) {
5361
+ console.log("No org roles defined. Create one: railcode roles create --name <name>.");
5362
+ return;
5363
+ }
5364
+ console.log(roleTable(roles));
5365
+ }
5366
+ async function commandRolesCreate(args) {
5367
+ rejectUnknownOptions(args, ["name", "description", "apiUrl"], "roles", ROLES_HELP);
5368
+ const name = optionString(args, "name");
5369
+ if (!name)
5370
+ throw new CliError("railcode roles create requires --name.", 2);
5371
+ const config = requireLogin(args);
5372
+ const role = (await authedJson(config, `/api/organizations/${config.orgUuid}/roles`, "Create role", jsonBody({ name, description: optionString(args, "description") ?? "" })));
5373
+ console.log(`Role "${role.name}" created (${role.uuid}).`);
5374
+ }
5375
+ async function commandRolesUpdate(args) {
5376
+ rejectUnknownOptions(args, ["name", "description", "apiUrl"], "roles", ROLES_HELP);
5377
+ const ref = args.rest[1];
5378
+ if (!ref)
5379
+ throw new CliError("Usage: railcode roles update <role> [--name <n>] [--description <d>]", 2);
5380
+ const body = {};
5381
+ const name = optionString(args, "name");
5382
+ const description = optionString(args, "description");
5383
+ if (name !== undefined)
5384
+ body.name = name;
5385
+ if (description !== undefined)
5386
+ body.description = description;
5387
+ if (Object.keys(body).length === 0) {
5388
+ throw new CliError("Nothing to update. Pass --name and/or --description.", 2);
5389
+ }
5390
+ const config = requireLogin(args);
5391
+ const role = resolveRoleRef((await fetchPermissions(config)).roles, ref);
5392
+ const updated = (await authedJson(config, `/api/organizations/${config.orgUuid}/roles/${role.uuid}`, "Update role", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }));
5393
+ console.log(`Role "${updated.name}" updated.`);
5394
+ }
5395
+ async function commandRolesDelete(args) {
5396
+ rejectUnknownOptions(args, ["apiUrl"], "roles", ROLES_HELP);
5397
+ const ref = args.rest[1];
5398
+ if (!ref)
5399
+ throw new CliError("Usage: railcode roles delete <role>", 2);
5400
+ const config = requireLogin(args);
5401
+ const role = resolveRoleRef((await fetchPermissions(config)).roles, ref);
5402
+ await authedNoContent(config, `/api/organizations/${config.orgUuid}/roles/${role.uuid}`, `Delete role "${role.name}"`, { method: "DELETE" });
5403
+ console.log(`Role "${role.name}" deleted (its memberships + grants were removed too).`);
5404
+ }
5405
+ async function commandRolesMember(args, add) {
5406
+ rejectUnknownOptions(args, ["apiUrl"], "roles", ROLES_HELP);
5407
+ const roleRef = args.rest[1];
5408
+ const userRef = args.rest[2];
5409
+ if (!roleRef || !userRef) {
5410
+ throw new CliError(`Usage: railcode roles ${add ? "add-member" : "remove-member"} <role> <email|uuid>`, 2);
5411
+ }
5412
+ const config = requireLogin(args);
5413
+ const role = resolveRoleRef((await fetchPermissions(config)).roles, roleRef);
5414
+ const member = resolveMemberRef(await fetchMembers(config), userRef);
5415
+ const base = `/api/organizations/${config.orgUuid}/roles/${role.uuid}/members`;
5416
+ if (add) {
5417
+ await authedNoContent(config, base, `Add ${member.email} to "${role.name}"`, jsonBody({ user_uuid: member.uuid }));
5418
+ console.log(`Added ${member.email} to role "${role.name}".`);
5419
+ }
5420
+ else {
5421
+ await authedNoContent(config, `${base}/${member.uuid}`, `Remove ${member.email} from "${role.name}"`, {
5422
+ method: "DELETE",
5423
+ });
5424
+ console.log(`Removed ${member.email} from role "${role.name}".`);
5425
+ }
5426
+ }
5427
+ async function commandRolesGrants(args) {
5428
+ rejectUnknownOptions(args, ["json", "apiUrl"], "roles", ROLES_HELP);
5429
+ const config = requireLogin(args);
5430
+ const { roles, grants } = await fetchPermissions(config);
5431
+ if (args.options.json) {
5432
+ console.log(JSON.stringify(grants, null, 2));
5433
+ return;
5434
+ }
5435
+ if (grants.length === 0) {
5436
+ console.log("No grants configured.");
5437
+ return;
5438
+ }
5439
+ const roleNames = new Map(roles.map((r) => [r.uuid, r.name]));
5440
+ const members = await fetchMembers(config);
5441
+ const userEmails = new Map(members.map((m) => [m.uuid, m.email]));
5442
+ const apps = await fetchApps(config);
5443
+ const appSlugs = new Map(apps.map((a) => [a.uuid, a.app_slug]));
5444
+ console.log(grantTable(grants, roleNames, userEmails, appSlugs));
5445
+ }
5446
+ async function commandRolesGrant(args) {
5447
+ rejectUnknownOptions(args, ["subject", "resource", "ids", "apiUrl"], "roles", ROLES_HELP);
5448
+ const spec = parseGrantSubject(optionString(args, "subject"));
5449
+ const resourceType = parseResourceType(optionString(args, "resource"));
5450
+ const resourceRefs = parseResourceIds(optionString(args, "ids"));
5451
+ const config = requireLogin(args);
5452
+ const subject = await resolveGrantSubject(config, spec);
5453
+ let resourceIds = resourceRefs;
5454
+ if (resourceType === "app") {
5455
+ if (resourceRefs.includes("*")) {
5456
+ throw new CliError("Apps have no wildcard — grant them one by one.", 2);
5457
+ }
5458
+ const apps = await fetchApps(config);
5459
+ resourceIds = resourceRefs.map((ref) => resolveAppRef(apps, ref).uuid);
5460
+ }
5461
+ const created = (await authedJson(config, `/api/organizations/${config.orgUuid}/permissions/grants`, "Create grant", jsonBody({ ...subject, resource_type: resourceType, resource_ids: resourceIds })));
5462
+ if (created.length === 0) {
5463
+ console.log("No new grants created (they already existed).");
5464
+ return;
5465
+ }
5466
+ console.log(`Granted ${resourceType} [${resourceRefs.join(", ")}] (${created.length} row(s) added).`);
5467
+ }
5468
+ async function commandRolesRevoke(args) {
5469
+ rejectUnknownOptions(args, ["apiUrl"], "roles", ROLES_HELP);
5470
+ const id = args.rest[1];
5471
+ if (!id || !/^\d+$/.test(id)) {
5472
+ throw new CliError("Usage: railcode roles revoke <grant_id> (see `railcode roles grants`).", 2);
5473
+ }
5474
+ const config = requireLogin(args);
5475
+ await authedNoContent(config, `/api/organizations/${config.orgUuid}/permissions/grants/${id}`, `Revoke grant ${id}`, { method: "DELETE" });
5476
+ console.log(`Grant ${id} revoked.`);
5477
+ }
5478
+ async function commandRolesMaterialize(args) {
5479
+ rejectUnknownOptions(args, ["subject", "resource", "apiUrl"], "roles", ROLES_HELP);
5480
+ const spec = parseGrantSubject(optionString(args, "subject"));
5481
+ const resourceType = parseResourceType(optionString(args, "resource"));
5482
+ const config = requireLogin(args);
5483
+ const subject = await resolveGrantSubject(config, spec);
5484
+ const created = (await authedJson(config, `/api/organizations/${config.orgUuid}/permissions/grants/materialize`, "Materialize wildcard", jsonBody({ ...subject, resource_type: resourceType })));
5485
+ console.log(`Materialized ${resourceType} wildcard into ${created.length} explicit grant(s).`);
5486
+ }
5487
+ async function commandRolesEffective(args) {
5488
+ rejectUnknownOptions(args, ["json", "apiUrl"], "roles", ROLES_HELP);
5489
+ const ref = args.rest[1];
5490
+ if (!ref)
5491
+ throw new CliError("Usage: railcode roles effective <email|uuid>", 2);
5492
+ const config = requireLogin(args);
5493
+ const member = resolveMemberRef(await fetchMembers(config), ref);
5494
+ const eff = (await authedJson(config, `/api/organizations/${config.orgUuid}/permissions/effective/${member.uuid}`, "Effective access"));
5495
+ if (args.options.json) {
5496
+ console.log(JSON.stringify(eff, null, 2));
5497
+ return;
5498
+ }
5499
+ console.log(renderEffective(eff, member.email));
5500
+ }
5501
+ async function commandRolesCatalog(args) {
5502
+ rejectUnknownOptions(args, ["json", "apiUrl"], "roles", ROLES_HELP);
5503
+ const config = requireLogin(args);
5504
+ const catalog = (await authedJson(config, `/api/organizations/${config.orgUuid}/permissions/catalog`, "Grant catalog"));
5505
+ if (args.options.json) {
5506
+ console.log(JSON.stringify(catalog, null, 2));
5507
+ return;
5508
+ }
5509
+ const line = (label, items) => console.log(`${label}: ${items.length ? items.join(", ") : "-"}`);
5510
+ line("apps", catalog.apps.map((a) => a.app_slug));
5511
+ line("connections", catalog.connections.map((c) => c.name));
5512
+ line("service_connectors", catalog.service_connectors.map((c) => c.name));
5513
+ line("saved_queries", catalog.saved_queries.map((q) => q.name));
5514
+ line("agents", catalog.agents.map((a) => a.name));
5515
+ }
5516
+ // ---------------------------------------------------------------------------
5517
+ // apps — app management + access policy (`railcode apps`)
5518
+ // ---------------------------------------------------------------------------
5519
+ const APPS_HELP = `railcode apps — apps + access policy
5520
+
5521
+ Usage:
5522
+ railcode apps list List apps you can see
5523
+ railcode apps show <app> Show one app's details
5524
+ railcode apps access <app> Show an app's access policy + grants
5525
+ railcode apps set-access <app> --mode organization|private|restricted
5526
+ [--members <email,email>] Set the policy (members: restricted only)
5527
+ railcode apps transfer <app> --to <email|uuid> Transfer ownership
5528
+ railcode apps delete <app> [--yes] Delete an app (deploys + data, irreversible)
5529
+
5530
+ <app> is an app slug or UUID. set-access/transfer/delete need manage rights
5531
+ (owner or org admin). delete prompts for confirmation on a TTY; pass --yes to skip
5532
+ (required in non-interactive sessions).
5533
+
5534
+ Options:
5535
+ --json Print raw JSON
5536
+ --yes Skip the delete confirmation prompt
5537
+ `;
5538
+ async function commandApps(args) {
5539
+ try {
5540
+ const sub = args.rest[0] ?? "";
5541
+ switch (sub) {
5542
+ case "list":
5543
+ case "ls":
5544
+ await commandAppsList(args);
5545
+ return;
5546
+ case "show":
5547
+ case "get":
5548
+ await commandAppsShow(args);
5549
+ return;
5550
+ case "access":
5551
+ await commandAppsAccess(args);
5552
+ return;
5553
+ case "set-access":
5554
+ await commandAppsSetAccess(args);
5555
+ return;
5556
+ case "transfer":
5557
+ await commandAppsTransfer(args);
5558
+ return;
5559
+ case "delete":
5560
+ case "rm":
5561
+ await commandAppsDelete(args);
5562
+ return;
5563
+ case "":
5564
+ case "help":
5565
+ console.log(APPS_HELP);
5566
+ return;
5567
+ default:
5568
+ throw new CliError(`Unknown apps command: ${sub}\n\n${APPS_HELP}`, 2);
5569
+ }
5570
+ }
5571
+ catch (error) {
5572
+ if (error instanceof OrgAdminError)
5573
+ throw new CliError(error.message, 2);
5574
+ throw error;
5575
+ }
5576
+ }
5577
+ async function commandAppsList(args) {
5578
+ rejectUnknownOptions(args, ["json", "apiUrl"], "apps", APPS_HELP);
5579
+ const config = requireLogin(args);
5580
+ const apps = await fetchApps(config);
5581
+ if (args.options.json) {
5582
+ console.log(JSON.stringify(apps, null, 2));
5583
+ return;
5584
+ }
5585
+ if (apps.length === 0) {
5586
+ console.log("No apps yet. Scaffold one: railcode init <app>.");
5587
+ return;
5588
+ }
5589
+ console.log(appTable(apps));
5590
+ }
5591
+ async function commandAppsShow(args) {
5592
+ rejectUnknownOptions(args, ["json", "apiUrl"], "apps", APPS_HELP);
5593
+ const ref = args.rest[1];
5594
+ if (!ref)
5595
+ throw new CliError("Usage: railcode apps show <app>", 2);
5596
+ const config = requireLogin(args);
5597
+ const app = resolveAppRef(await fetchApps(config), ref);
5598
+ const full = (await authedJson(config, `/api/organizations/${config.orgUuid}/apps/${app.uuid}`, "Get app"));
5599
+ if (args.options.json) {
5600
+ console.log(JSON.stringify(full, null, 2));
5601
+ return;
5602
+ }
5603
+ const row = (label, value) => ` ${`${label}:`.padEnd(14)}${value}`;
5604
+ console.log(`${full.app_slug} (${full.name})`);
5605
+ console.log(row("uuid", String(full.uuid)));
5606
+ console.log(row("status", String(full.status)));
5607
+ console.log(row("access", String(full.access_mode)));
5608
+ console.log(row("your role", full.your_role ?? "-"));
5609
+ console.log(row("can manage", full.can_manage ? "yes" : "no"));
5610
+ console.log(row("has manifest", full.has_manifest ? "yes" : "no"));
5611
+ if (full.last_deployed_at)
5612
+ console.log(row("deployed", String(full.last_deployed_at)));
5613
+ }
5614
+ async function commandAppsAccess(args) {
5615
+ rejectUnknownOptions(args, ["json", "apiUrl"], "apps", APPS_HELP);
5616
+ const ref = args.rest[1];
5617
+ if (!ref)
5618
+ throw new CliError("Usage: railcode apps access <app>", 2);
5619
+ const config = requireLogin(args);
5620
+ const app = resolveAppRef(await fetchApps(config), ref);
5621
+ const access = (await authedJson(config, `/api/organizations/${config.orgUuid}/apps/${app.uuid}/access`, "Get app access"));
5622
+ // App access has two sources: the app's own policy (mode + per-user grants,
5623
+ // above) AND grant-substrate rows (`roles grant --resource app`, plus the
5624
+ // access granted to apps/agents). The policy view alone is incomplete, so also
5625
+ // surface any org_grants that open THIS app.
5626
+ const { roles, grants } = await fetchPermissions(config);
5627
+ const substrate = grants.filter((g) => g.resource_type === "app" && g.resource_id === app.uuid);
5628
+ if (args.options.json) {
5629
+ console.log(JSON.stringify({ ...access, grants: access.grants, org_grants: substrate }, null, 2));
5630
+ return;
5631
+ }
5632
+ console.log(renderAppAccess(app.app_slug, access));
5633
+ if (substrate.length > 0) {
5634
+ const roleNames = new Map(roles.map((r) => [r.uuid, r.name]));
5635
+ const members = await fetchMembers(config);
5636
+ const userEmails = new Map(members.map((m) => [m.uuid, m.email]));
5637
+ console.log(" also granted access via roles/grants:");
5638
+ for (const g of substrate) {
5639
+ console.log(` ${grantSubjectLabel(g, roleNames, userEmails)}`);
5640
+ }
5641
+ }
5642
+ }
5643
+ async function commandAppsSetAccess(args) {
5644
+ rejectUnknownOptions(args, ["mode", "members", "json", "apiUrl"], "apps", APPS_HELP);
5645
+ const ref = args.rest[1];
5646
+ if (!ref)
5647
+ throw new CliError("Usage: railcode apps set-access <app> --mode <mode> [--members ...]", 2);
5648
+ const mode = parseAccessMode(optionString(args, "mode"));
5649
+ const memberRefs = parseMemberList(optionString(args, "members"));
5650
+ if (mode !== "restricted" && memberRefs.length > 0) {
5651
+ throw new CliError("--members only applies to --mode restricted.", 2);
5652
+ }
5653
+ const config = requireLogin(args);
5654
+ const app = resolveAppRef(await fetchApps(config), ref);
5655
+ let members = [];
5656
+ if (mode === "restricted" && memberRefs.length > 0) {
5657
+ const all = await fetchMembers(config);
5658
+ members = memberRefs.map((r) => resolveMemberRef(all, r).uuid);
5659
+ }
5660
+ const access = (await authedJson(config, `/api/organizations/${config.orgUuid}/apps/${app.uuid}/access`, "Set app access", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ mode, members }) }));
5661
+ if (args.options.json) {
5662
+ console.log(JSON.stringify(access, null, 2));
5663
+ return;
5664
+ }
5665
+ console.log(renderAppAccess(app.app_slug, access));
5666
+ }
5667
+ async function commandAppsTransfer(args) {
5668
+ rejectUnknownOptions(args, ["to", "apiUrl"], "apps", APPS_HELP);
5669
+ const ref = args.rest[1];
5670
+ const to = optionString(args, "to");
5671
+ if (!ref || !to)
5672
+ throw new CliError("Usage: railcode apps transfer <app> --to <email|uuid>", 2);
5673
+ const config = requireLogin(args);
5674
+ const app = resolveAppRef(await fetchApps(config), ref);
5675
+ const newOwner = resolveMemberRef(await fetchMembers(config), to);
5676
+ await authedJson(config, `/api/organizations/${config.orgUuid}/apps/${app.uuid}/owner`, "Transfer app owner", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_uuid: newOwner.uuid }) });
5677
+ console.log(`${app.app_slug} is now owned by ${newOwner.email}.`);
5678
+ }
5679
+ async function commandAppsDelete(args) {
5680
+ rejectUnknownOptions(args, ["yes", "apiUrl"], "apps", APPS_HELP);
5681
+ const ref = args.rest[1];
5682
+ if (!ref)
5683
+ throw new CliError("Usage: railcode apps delete <app>", 2);
5684
+ const config = requireLogin(args);
5685
+ const app = resolveAppRef(await fetchApps(config), ref);
5686
+ if (args.options.yes !== true) {
5687
+ if (!process.stdin.isTTY) {
5688
+ throw new CliError("Pass --yes to delete an app in a non-interactive session.", 2);
5689
+ }
5690
+ const answer = await prompt(`Delete app "${app.app_slug}"? This removes its deploys + data and cannot be undone. [y/N] `);
5691
+ if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
5692
+ throw new CliError("Delete cancelled.", 1);
5693
+ }
5694
+ }
5695
+ await authedNoContent(config, `/api/organizations/${config.orgUuid}/apps/${app.uuid}`, `Delete app "${app.app_slug}"`, { method: "DELETE" });
5696
+ console.log(`App "${app.app_slug}" deleted.`);
5697
+ }
5698
+ // ---------------------------------------------------------------------------
5699
+ // analytics — per-app pageviews (`railcode analytics <app>`)
5700
+ // ---------------------------------------------------------------------------
5701
+ const ANALYTICS_HELP = `railcode analytics — per-app pageview analytics (admin/owner)
5702
+
5703
+ Usage:
5704
+ railcode analytics <app> [--range 1d|7d|30d|90d] Views, uniques, daily, top paths/users
5705
+
5706
+ <app> is an app slug or UUID. Default range 30d.
5707
+
5708
+ Options:
5709
+ --json Print the raw analytics object
5710
+ `;
5711
+ async function commandAnalytics(args) {
5712
+ try {
5713
+ const ref = args.rest[0];
5714
+ if (!ref || ref === "help") {
5715
+ console.log(ANALYTICS_HELP);
5716
+ return;
5717
+ }
5718
+ rejectUnknownOptions(args, ["range", "json", "apiUrl"], "analytics", ANALYTICS_HELP);
5719
+ const range = parseRange(optionString(args, "range"));
5720
+ const config = requireLogin(args);
5721
+ const app = resolveAppRef(await fetchApps(config), ref);
5722
+ const qs = range ? `?range=${range}` : "";
5723
+ const data = (await authedJson(config, `/api/organizations/${config.orgUuid}/apps/${app.uuid}/analytics${qs}`, "Get analytics"));
5724
+ if (args.options.json) {
5725
+ console.log(JSON.stringify(data, null, 2));
5726
+ return;
5727
+ }
5728
+ console.log(renderAnalytics(app.app_slug, data));
5729
+ }
5730
+ catch (error) {
5731
+ if (error instanceof OrgAdminError || error instanceof ObservabilityError) {
5732
+ throw new CliError(error.message, 2);
5733
+ }
5734
+ throw error;
5735
+ }
5736
+ }
5737
+ // ---------------------------------------------------------------------------
5738
+ // logs — org observability streams (`railcode logs <stream>`)
5739
+ // ---------------------------------------------------------------------------
5740
+ const LOGS_HELP = `railcode logs — org observability (admin)
5741
+
5742
+ Usage:
5743
+ railcode logs <stream> [filters] Recent log rows (newest first)
5744
+ railcode logs <stream> <request_id> Full detail for one entry
5745
+
5746
+ Streams:
5747
+ connector data-connection SQL queries (connection:manage)
5748
+ service-connector service-connector proxy calls (service-connector:manage)
5749
+ llm LLM gateway calls (llm:manage)
5750
+ email email gateway sends (email:manage)
5751
+ agent managed-agent runs (agent:manage)
5752
+
5753
+ Filters (each optional; unsupported ones are rejected per stream):
5754
+ --app <slug|uuid> Only rows for this app
5755
+ --user <email|uuid> Only rows for this user
5756
+ --connector <name> Only this connector (connector/service-connector)
5757
+ --agent <uuid> Only this agent (agent stream)
5758
+ --source app|agent LLM source (llm stream)
5759
+ --status <value> Stream-specific status (e.g. success|error)
5760
+ --limit <1..500> Max rows (default 100)
5761
+ --json Print raw JSON
5762
+ `;
5763
+ async function commandLogs(args) {
5764
+ try {
5765
+ const streamName = args.rest[0] ?? "";
5766
+ if (streamName === "" || streamName === "help") {
5767
+ console.log(LOGS_HELP);
5768
+ return;
5769
+ }
5770
+ rejectUnknownOptions(args, ["app", "user", "connector", "agent", "source", "status", "limit", "json", "apiUrl"], "logs", LOGS_HELP);
5771
+ const stream = resolveLogStream(streamName);
5772
+ const requestId = args.rest[1];
5773
+ if (args.rest.length > 2) {
5774
+ throw new CliError(`railcode logs ${stream.key} takes at most one request id.`, 2);
5775
+ }
5776
+ const config = requireLogin(args);
5777
+ if (requestId) {
5778
+ await logsShowDetail(config, stream, requestId, args);
5779
+ return;
5780
+ }
5781
+ await logsList(config, stream, args);
5782
+ }
5783
+ catch (error) {
5784
+ if (error instanceof ObservabilityError || error instanceof OrgAdminError) {
5785
+ throw new CliError(error.message, 2);
5786
+ }
5787
+ throw error;
5788
+ }
5789
+ }
5790
+ // Turn --app/--user/--agent references into UUIDs the API expects. Slugs/emails are
5791
+ // resolved against the org's apps/members; a bare UUID passes through unchanged.
5792
+ async function resolveLogFilters(config, stream, args) {
5793
+ const out = {};
5794
+ const appRef = optionString(args, "app");
5795
+ const userRef = optionString(args, "user");
5796
+ const agentRef = optionString(args, "agent");
5797
+ const connector = optionString(args, "connector");
5798
+ const source = optionString(args, "source");
5799
+ const status = optionString(args, "status");
5800
+ const ensure = (key) => {
5801
+ if (!stream.filters.includes(key)) {
5802
+ throw new ObservabilityError(`--${key} is not a valid filter for ${stream.key} logs.`);
5803
+ }
5804
+ };
5805
+ if (appRef !== undefined) {
5806
+ ensure("app");
5807
+ out.app = /^[0-9a-f-]{36}$/i.test(appRef) ? appRef : resolveAppRef(await fetchApps(config), appRef).uuid;
5808
+ }
5809
+ if (userRef !== undefined) {
5810
+ ensure("user");
5811
+ out.user = /^[0-9a-f-]{36}$/i.test(userRef) ? userRef : resolveMemberRef(await fetchMembers(config), userRef).uuid;
5812
+ }
5813
+ if (agentRef !== undefined) {
5814
+ ensure("agent");
5815
+ out.agent = agentRef;
5816
+ }
5817
+ if (connector !== undefined) {
5818
+ ensure("connector");
5819
+ out.connector = connector;
5820
+ }
5821
+ if (source !== undefined) {
5822
+ ensure("source");
5823
+ out.source = validateLogSource(stream, source);
5824
+ }
5825
+ if (status !== undefined) {
5826
+ ensure("status");
5827
+ out.status = validateLogStatus(stream, status);
5828
+ }
5829
+ out.limit = parseLimit(optionString(args, "limit"));
5830
+ return out;
5831
+ }
5832
+ async function logsList(config, stream, args) {
5833
+ const filters = await resolveLogFilters(config, stream, args);
5834
+ const rows = (await authedJson(config, `/api/organizations/${config.orgUuid}${stream.path}${buildLogQuery(filters)}`, `List ${stream.key} logs`));
5835
+ if (args.options.json) {
5836
+ console.log(JSON.stringify(rows, null, 2));
5837
+ return;
5838
+ }
5839
+ if (rows.length === 0) {
5840
+ console.log(`No ${stream.label} logged (matching your filters).`);
5841
+ return;
5842
+ }
5843
+ console.log(logTable(stream, rows));
5844
+ }
5845
+ async function logsShowDetail(config, stream, requestId, args) {
5846
+ const detail = await authedJson(config, `/api/organizations/${config.orgUuid}${stream.path}/${encodeURIComponent(requestId)}`, `Get ${stream.key} log`);
5847
+ // Detail payloads vary a lot per stream; JSON is the faithful, complete view.
5848
+ console.log(JSON.stringify(detail, null, 2));
5849
+ void args;
5850
+ }
5851
+ // ---------------------------------------------------------------------------
5852
+ // connections — org data connectors (`railcode connections`)
5853
+ // ---------------------------------------------------------------------------
5854
+ const CONNECTIONS_HELP = `railcode connections — org data connectors (admin)
5855
+
5856
+ Usage:
5857
+ railcode connections list List data connectors
5858
+ railcode connections create --name <n> --kind postgres|bigquery|turso
5859
+ --config <json> --credentials <json>
5860
+ [--config-file <f>] [--credentials-file <f>]
5861
+ Create/replace a connector (dials before saving)
5862
+ railcode connections delete <name|uuid> Delete a connector
5863
+
5864
+ Config/credentials fields per engine:
5865
+ postgres config {host,database,username,port?,sslmode?} credentials {password}
5866
+ bigquery config {project_id,dataset} credentials {service_account_json}
5867
+ turso config {url} credentials {auth_token}
5868
+
5869
+ Options:
5870
+ --json Print raw JSON (list)
5871
+ `;
5872
+ async function commandConnections(args) {
5873
+ try {
5874
+ const sub = args.rest[0] ?? "";
5875
+ switch (sub) {
5876
+ case "list":
5877
+ case "ls":
5878
+ await commandConnectionsList(args);
5879
+ return;
5880
+ case "create":
5881
+ await commandConnectionsCreate(args);
5882
+ return;
5883
+ case "delete":
5884
+ case "rm":
5885
+ await commandConnectionsDelete(args);
5886
+ return;
5887
+ case "":
5888
+ case "help":
5889
+ console.log(CONNECTIONS_HELP);
5890
+ return;
5891
+ default:
5892
+ throw new CliError(`Unknown connections command: ${sub}\n\n${CONNECTIONS_HELP}`, 2);
5893
+ }
5894
+ }
5895
+ catch (error) {
5896
+ if (error instanceof ConnectionAdminError)
5897
+ throw new CliError(error.message, 2);
5898
+ throw error;
5899
+ }
5900
+ }
5901
+ async function fetchConnections(config) {
5902
+ return (await authedJson(config, `/api/organizations/${config.orgUuid}/connections`, "List connections"));
5903
+ }
5904
+ async function commandConnectionsList(args) {
5905
+ rejectUnknownOptions(args, ["json", "apiUrl"], "connections", CONNECTIONS_HELP);
5906
+ const config = requireLogin(args);
5907
+ const conns = await fetchConnections(config);
5908
+ if (args.options.json) {
5909
+ console.log(JSON.stringify(conns, null, 2));
5910
+ return;
5911
+ }
5912
+ if (conns.length === 0) {
5913
+ console.log("No data connectors configured for this organization.");
5914
+ return;
5915
+ }
5916
+ console.log(connectionTable(conns));
5917
+ }
5918
+ async function commandConnectionsCreate(args) {
5919
+ rejectUnknownOptions(args, ["name", "kind", "config", "configFile", "credentials", "credentialsFile", "apiUrl"], "connections", CONNECTIONS_HELP);
5920
+ const kind = parseConnectionEngine(optionString(args, "kind"));
5921
+ const configObj = readJsonObjectOption(args, "config", "configFile", "--config");
5922
+ const credentials = readJsonObjectOption(args, "credentials", "credentialsFile", "--credentials");
5923
+ const body = buildConnectionCreateBody({
5924
+ name: optionString(args, "name"),
5925
+ kind,
5926
+ config: configObj,
5927
+ credentials,
5928
+ });
5929
+ const config = requireLogin(args);
5930
+ const created = (await authedJson(config, `/api/organizations/${config.orgUuid}/connections`, "Create connection", jsonBody(body)));
5931
+ console.log(`Connection "${created.name}" (${created.kind}) → ${created.target}.`);
5932
+ }
5933
+ async function commandConnectionsDelete(args) {
5934
+ rejectUnknownOptions(args, ["apiUrl"], "connections", CONNECTIONS_HELP);
5935
+ const ref = args.rest[1];
5936
+ if (!ref)
5937
+ throw new CliError("Usage: railcode connections delete <name|uuid>", 2);
5938
+ const config = requireLogin(args);
5939
+ const conn = resolveConnectionRef(await fetchConnections(config), ref);
5940
+ await authedNoContent(config, `/api/organizations/${config.orgUuid}/connections/${conn.uuid}`, `Delete connection "${conn.name}"`, { method: "DELETE" });
5941
+ console.log(`Connection "${conn.name}" deleted (its saved queries + grants were removed too).`);
5942
+ }
5943
+ // Read a JSON-object option from an inline `--foo` value or a `--foo-file` path.
5944
+ function readJsonObjectOption(args, inlineKey, fileKey, label) {
5945
+ const inline = optionString(args, inlineKey);
5946
+ const file = optionString(args, fileKey);
5947
+ if (inline !== undefined && file !== undefined) {
5948
+ throw new CliError(`Pass either ${label} or ${label}-file, not both.`, 2);
5949
+ }
5950
+ if (file !== undefined) {
5951
+ const resolved = resolve(process.cwd(), file);
5952
+ if (!existsSync(resolved))
5953
+ throw new CliError(`${label}-file not found: ${file}`, 2);
5954
+ return parseJsonObject(readFileSync(resolved, "utf8"), `${label}-file`);
5955
+ }
5956
+ return parseJsonObject(inline, label);
5957
+ }
5958
+ // ---------------------------------------------------------------------------
4957
5959
  // Config persistence
4958
5960
  // ---------------------------------------------------------------------------
4959
5961
  function loadConfig() {