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/README.md +17 -0
- package/dist/connections.js +179 -0
- package/dist/connectors.js +4 -1
- package/dist/index.js +1005 -3
- package/dist/observability.js +220 -0
- package/dist/orgadmin.js +227 -0
- package/package.json +1 -1
- package/static/sdk.js +76 -8
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> ...
|
|
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
|
-
|
|
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() {
|