@carthooks/arcubase-cli 0.1.0 → 0.1.2
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/bundle/arcubase-admin.mjs +17 -3
- package/bundle/arcubase.mjs +17 -3
- package/dist/runtime/env.d.ts +1 -0
- package/dist/runtime/env.d.ts.map +1 -1
- package/dist/runtime/env.js +1 -0
- package/dist/runtime/execute.js +4 -4
- package/dist/runtime/http.d.ts +2 -1
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +14 -1
- package/dist/tests/bootstrap.test.js +5 -0
- package/dist/tests/execute_validation.test.js +10 -2
- package/dist/tests/help.test.js +7 -1
- package/package.json +1 -4
- package/src/runtime/env.ts +2 -0
- package/src/runtime/execute.ts +4 -4
- package/src/runtime/http.ts +16 -2
- package/src/tests/bootstrap.test.ts +5 -0
- package/src/tests/execute_validation.test.ts +16 -2
- package/src/tests/help.test.ts +8 -1
|
@@ -94,6 +94,7 @@ function loadRuntimeEnv(scope, env = process.env) {
|
|
|
94
94
|
ARCUBASE_BASE_URL: requiredValue(env, scope, "ARCUBASE_BASE_URL"),
|
|
95
95
|
ARCUBASE_TENANT_ID: requiredValue(env, scope, "ARCUBASE_TENANT_ID"),
|
|
96
96
|
ARCUBASE_ACCESS_TOKEN: requiredValue(env, scope, "ARCUBASE_ACCESS_TOKEN"),
|
|
97
|
+
ARCUBASE_HOST: optionalValue(env, scope, "ARCUBASE_HOST"),
|
|
97
98
|
ARCUBASE_TRACE_ID: optionalValue(env, scope, "ARCUBASE_TRACE_ID"),
|
|
98
99
|
ARCUBASE_SUBJECT_TYPE: optionalValue(env, scope, "ARCUBASE_SUBJECT_TYPE"),
|
|
99
100
|
ARCUBASE_SUBJECT_ID: optionalValue(env, scope, "ARCUBASE_SUBJECT_ID"),
|
|
@@ -107,13 +108,26 @@ function buildRequestHeaders(env) {
|
|
|
107
108
|
Authorization: `Bearer ${env.ARCUBASE_ACCESS_TOKEN}`,
|
|
108
109
|
"X-Tenant-Id": env.ARCUBASE_TENANT_ID
|
|
109
110
|
};
|
|
111
|
+
if (env.ARCUBASE_HOST && env.ARCUBASE_HOST.trim() !== "") {
|
|
112
|
+
headers["X-Host"] = env.ARCUBASE_HOST;
|
|
113
|
+
}
|
|
110
114
|
if (env.ARCUBASE_TRACE_ID) {
|
|
111
115
|
headers["X-Trace-Id"] = env.ARCUBASE_TRACE_ID;
|
|
112
116
|
}
|
|
113
117
|
return headers;
|
|
114
118
|
}
|
|
119
|
+
function normalizeExternalEndpoint(endpoint) {
|
|
120
|
+
const trimmed = endpoint.trim();
|
|
121
|
+
if (trimmed === "/api") {
|
|
122
|
+
return "/";
|
|
123
|
+
}
|
|
124
|
+
if (trimmed.startsWith("/api/")) {
|
|
125
|
+
return trimmed.slice(4);
|
|
126
|
+
}
|
|
127
|
+
return trimmed;
|
|
128
|
+
}
|
|
115
129
|
function buildURL(baseURL, endpoint, query) {
|
|
116
|
-
const url = new URL(endpoint, baseURL.endsWith("/") ? baseURL : `${baseURL}/`);
|
|
130
|
+
const url = new URL(normalizeExternalEndpoint(endpoint), baseURL.endsWith("/") ? baseURL : `${baseURL}/`);
|
|
117
131
|
if (query) {
|
|
118
132
|
for (const [key, value] of Object.entries(query)) {
|
|
119
133
|
if (value === void 0 || value === null) continue;
|
|
@@ -9711,7 +9725,7 @@ function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
9711
9725
|
return [
|
|
9712
9726
|
`${scope === "admin" ? "arcubase-admin" : "arcubase"} ${moduleName} ${commandName}`,
|
|
9713
9727
|
`method: ${command.method}`,
|
|
9714
|
-
`endpoint: ${command.endpoint}`,
|
|
9728
|
+
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
9715
9729
|
command.requestType ? `body: ${command.requestType} via --body-file` : "body: none",
|
|
9716
9730
|
command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, "-")}`).join(", ")}` : "path flags: none",
|
|
9717
9731
|
command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, "-$1").toLowerCase()}`).join(", ")}` : "scalar flags: none"
|
|
@@ -9777,7 +9791,7 @@ async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetchImpl =
|
|
|
9777
9791
|
if (!command) {
|
|
9778
9792
|
throw new CLIError("UNKNOWN_COMMAND", `unknown command: ${moduleName} ${commandName}`, 2);
|
|
9779
9793
|
}
|
|
9780
|
-
const endpoint = resolveEndpoint(command.endpoint, parsed.flags);
|
|
9794
|
+
const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags));
|
|
9781
9795
|
const scalarQuery = buildScalarQuery(command, parsed.flags);
|
|
9782
9796
|
const fileQuery = loadQueryFromFlags(parsed.flags);
|
|
9783
9797
|
const query = { ...scalarQuery ?? {}, ...fileQuery ?? {} };
|
package/bundle/arcubase.mjs
CHANGED
|
@@ -94,6 +94,7 @@ function loadRuntimeEnv(scope, env = process.env) {
|
|
|
94
94
|
ARCUBASE_BASE_URL: requiredValue(env, scope, "ARCUBASE_BASE_URL"),
|
|
95
95
|
ARCUBASE_TENANT_ID: requiredValue(env, scope, "ARCUBASE_TENANT_ID"),
|
|
96
96
|
ARCUBASE_ACCESS_TOKEN: requiredValue(env, scope, "ARCUBASE_ACCESS_TOKEN"),
|
|
97
|
+
ARCUBASE_HOST: optionalValue(env, scope, "ARCUBASE_HOST"),
|
|
97
98
|
ARCUBASE_TRACE_ID: optionalValue(env, scope, "ARCUBASE_TRACE_ID"),
|
|
98
99
|
ARCUBASE_SUBJECT_TYPE: optionalValue(env, scope, "ARCUBASE_SUBJECT_TYPE"),
|
|
99
100
|
ARCUBASE_SUBJECT_ID: optionalValue(env, scope, "ARCUBASE_SUBJECT_ID"),
|
|
@@ -107,13 +108,26 @@ function buildRequestHeaders(env) {
|
|
|
107
108
|
Authorization: `Bearer ${env.ARCUBASE_ACCESS_TOKEN}`,
|
|
108
109
|
"X-Tenant-Id": env.ARCUBASE_TENANT_ID
|
|
109
110
|
};
|
|
111
|
+
if (env.ARCUBASE_HOST && env.ARCUBASE_HOST.trim() !== "") {
|
|
112
|
+
headers["X-Host"] = env.ARCUBASE_HOST;
|
|
113
|
+
}
|
|
110
114
|
if (env.ARCUBASE_TRACE_ID) {
|
|
111
115
|
headers["X-Trace-Id"] = env.ARCUBASE_TRACE_ID;
|
|
112
116
|
}
|
|
113
117
|
return headers;
|
|
114
118
|
}
|
|
119
|
+
function normalizeExternalEndpoint(endpoint) {
|
|
120
|
+
const trimmed = endpoint.trim();
|
|
121
|
+
if (trimmed === "/api") {
|
|
122
|
+
return "/";
|
|
123
|
+
}
|
|
124
|
+
if (trimmed.startsWith("/api/")) {
|
|
125
|
+
return trimmed.slice(4);
|
|
126
|
+
}
|
|
127
|
+
return trimmed;
|
|
128
|
+
}
|
|
115
129
|
function buildURL(baseURL, endpoint, query) {
|
|
116
|
-
const url = new URL(endpoint, baseURL.endsWith("/") ? baseURL : `${baseURL}/`);
|
|
130
|
+
const url = new URL(normalizeExternalEndpoint(endpoint), baseURL.endsWith("/") ? baseURL : `${baseURL}/`);
|
|
117
131
|
if (query) {
|
|
118
132
|
for (const [key, value] of Object.entries(query)) {
|
|
119
133
|
if (value === void 0 || value === null) continue;
|
|
@@ -9711,7 +9725,7 @@ function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
9711
9725
|
return [
|
|
9712
9726
|
`${scope === "admin" ? "arcubase-admin" : "arcubase"} ${moduleName} ${commandName}`,
|
|
9713
9727
|
`method: ${command.method}`,
|
|
9714
|
-
`endpoint: ${command.endpoint}`,
|
|
9728
|
+
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
9715
9729
|
command.requestType ? `body: ${command.requestType} via --body-file` : "body: none",
|
|
9716
9730
|
command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, "-")}`).join(", ")}` : "path flags: none",
|
|
9717
9731
|
command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, "-$1").toLowerCase()}`).join(", ")}` : "scalar flags: none"
|
|
@@ -9777,7 +9791,7 @@ async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetchImpl =
|
|
|
9777
9791
|
if (!command) {
|
|
9778
9792
|
throw new CLIError("UNKNOWN_COMMAND", `unknown command: ${moduleName} ${commandName}`, 2);
|
|
9779
9793
|
}
|
|
9780
|
-
const endpoint = resolveEndpoint(command.endpoint, parsed.flags);
|
|
9794
|
+
const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags));
|
|
9781
9795
|
const scalarQuery = buildScalarQuery(command, parsed.flags);
|
|
9782
9796
|
const fileQuery = loadQueryFromFlags(parsed.flags);
|
|
9783
9797
|
const query = { ...scalarQuery ?? {}, ...fileQuery ?? {} };
|
package/dist/runtime/env.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/runtime/env.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,yBAAyB,CAAC,EAAE,MAAM,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,CAAA;AAE9C,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAqClD,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,GAAE,QAAsB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../../src/runtime/env.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG;IACvB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,yBAAyB,CAAC,EAAE,MAAM,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,CAAA;AAE9C,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;AAqClD,wBAAgB,cAAc,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,GAAE,QAAsB,GAAG,UAAU,CAW9F"}
|
package/dist/runtime/env.js
CHANGED
|
@@ -34,6 +34,7 @@ export function loadRuntimeEnv(scope, env = process.env) {
|
|
|
34
34
|
ARCUBASE_BASE_URL: requiredValue(env, scope, 'ARCUBASE_BASE_URL'),
|
|
35
35
|
ARCUBASE_TENANT_ID: requiredValue(env, scope, 'ARCUBASE_TENANT_ID'),
|
|
36
36
|
ARCUBASE_ACCESS_TOKEN: requiredValue(env, scope, 'ARCUBASE_ACCESS_TOKEN'),
|
|
37
|
+
ARCUBASE_HOST: optionalValue(env, scope, 'ARCUBASE_HOST'),
|
|
37
38
|
ARCUBASE_TRACE_ID: optionalValue(env, scope, 'ARCUBASE_TRACE_ID'),
|
|
38
39
|
ARCUBASE_SUBJECT_TYPE: optionalValue(env, scope, 'ARCUBASE_SUBJECT_TYPE'),
|
|
39
40
|
ARCUBASE_SUBJECT_ID: optionalValue(env, scope, 'ARCUBASE_SUBJECT_ID'),
|
package/dist/runtime/execute.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseCLIArgs, hasFlag, flagValue } from './argv.js';
|
|
2
2
|
import { loadRuntimeEnv } from './env.js';
|
|
3
3
|
import { CLIError } from './errors.js';
|
|
4
|
-
import { buildRequestHeaders, buildURL } from './http.js';
|
|
4
|
+
import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http.js';
|
|
5
5
|
import { listModules, listModuleCommands, findCommand } from './command_registry.js';
|
|
6
6
|
import { loadBodyFromFlags, loadQueryFromFlags } from './body_loader.js';
|
|
7
7
|
import { getBodySchema, getTypeHints, getUnsupportedBodySchemaReason } from './zod_registry.js';
|
|
@@ -34,7 +34,7 @@ export function renderCommandHelp(scope, moduleName, commandName) {
|
|
|
34
34
|
return [
|
|
35
35
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} ${commandName}`,
|
|
36
36
|
`method: ${command.method}`,
|
|
37
|
-
`endpoint: ${command.endpoint}`,
|
|
37
|
+
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
38
38
|
command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
|
|
39
39
|
command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
|
|
40
40
|
command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
|
|
@@ -93,7 +93,7 @@ export function summarizeCommand(scope, moduleName, commandName) {
|
|
|
93
93
|
scope,
|
|
94
94
|
commandPath: command.commandPath,
|
|
95
95
|
method: command.method,
|
|
96
|
-
endpoint: command.endpoint,
|
|
96
|
+
endpoint: normalizeExternalEndpoint(command.endpoint),
|
|
97
97
|
requestType: command.requestType,
|
|
98
98
|
pathFlags: command.endpointParams,
|
|
99
99
|
scalarFlags: command.scalarParams.map((item) => item.name.replace(/([A-Z])/g, '-$1').toLowerCase()),
|
|
@@ -118,7 +118,7 @@ export async function executeCLI(scope, argv, env = loadRuntimeEnv(scope), fetch
|
|
|
118
118
|
if (!command) {
|
|
119
119
|
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2);
|
|
120
120
|
}
|
|
121
|
-
const endpoint = resolveEndpoint(command.endpoint, parsed.flags);
|
|
121
|
+
const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags));
|
|
122
122
|
const scalarQuery = buildScalarQuery(command, parsed.flags);
|
|
123
123
|
const fileQuery = loadQueryFromFlags(parsed.flags);
|
|
124
124
|
const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) };
|
package/dist/runtime/http.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RuntimeEnv } from './env.js';
|
|
2
|
-
export declare function buildRequestHeaders(env: Pick<RuntimeEnv, 'ARCUBASE_ACCESS_TOKEN' | 'ARCUBASE_TENANT_ID' | 'ARCUBASE_TRACE_ID'>): Record<string, string>;
|
|
2
|
+
export declare function buildRequestHeaders(env: Pick<RuntimeEnv, 'ARCUBASE_ACCESS_TOKEN' | 'ARCUBASE_TENANT_ID' | 'ARCUBASE_HOST' | 'ARCUBASE_TRACE_ID'>): Record<string, string>;
|
|
3
|
+
export declare function normalizeExternalEndpoint(endpoint: string): string;
|
|
3
4
|
export declare function buildURL(baseURL: string, endpoint: string, query?: Record<string, any>): string;
|
|
4
5
|
//# sourceMappingURL=http.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/runtime/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,uBAAuB,GAAG,oBAAoB,GAAG,mBAAmB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/runtime/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,uBAAuB,GAAG,oBAAoB,GAAG,eAAe,GAAG,mBAAmB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYzK;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CASlE;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAS/F"}
|
package/dist/runtime/http.js
CHANGED
|
@@ -3,13 +3,26 @@ export function buildRequestHeaders(env) {
|
|
|
3
3
|
Authorization: `Bearer ${env.ARCUBASE_ACCESS_TOKEN}`,
|
|
4
4
|
'X-Tenant-Id': env.ARCUBASE_TENANT_ID
|
|
5
5
|
};
|
|
6
|
+
if (env.ARCUBASE_HOST && env.ARCUBASE_HOST.trim() !== '') {
|
|
7
|
+
headers['X-Host'] = env.ARCUBASE_HOST;
|
|
8
|
+
}
|
|
6
9
|
if (env.ARCUBASE_TRACE_ID) {
|
|
7
10
|
headers['X-Trace-Id'] = env.ARCUBASE_TRACE_ID;
|
|
8
11
|
}
|
|
9
12
|
return headers;
|
|
10
13
|
}
|
|
14
|
+
export function normalizeExternalEndpoint(endpoint) {
|
|
15
|
+
const trimmed = endpoint.trim();
|
|
16
|
+
if (trimmed === '/api') {
|
|
17
|
+
return '/';
|
|
18
|
+
}
|
|
19
|
+
if (trimmed.startsWith('/api/')) {
|
|
20
|
+
return trimmed.slice(4);
|
|
21
|
+
}
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
11
24
|
export function buildURL(baseURL, endpoint, query) {
|
|
12
|
-
const url = new URL(endpoint, baseURL.endsWith('/') ? baseURL : `${baseURL}/`);
|
|
25
|
+
const url = new URL(normalizeExternalEndpoint(endpoint), baseURL.endsWith('/') ? baseURL : `${baseURL}/`);
|
|
13
26
|
if (query) {
|
|
14
27
|
for (const [key, value] of Object.entries(query)) {
|
|
15
28
|
if (value === undefined || value === null)
|
|
@@ -15,6 +15,7 @@ test('loadRuntimeEnv returns normalized runtime env', () => {
|
|
|
15
15
|
ARCUBASE_BASE_URL: 'http://example.com',
|
|
16
16
|
ARCUBASE_TENANT_ID: 'tenant-1',
|
|
17
17
|
ARCUBASE_ACCESS_TOKEN: 'token-1',
|
|
18
|
+
ARCUBASE_HOST: undefined,
|
|
18
19
|
ARCUBASE_TRACE_ID: 'trace-1',
|
|
19
20
|
ARCUBASE_SUBJECT_TYPE: undefined,
|
|
20
21
|
ARCUBASE_SUBJECT_ID: undefined,
|
|
@@ -26,6 +27,7 @@ test('loadRuntimeEnv accepts admin-prefixed runtime env', () => {
|
|
|
26
27
|
ARCUBASE_ADMIN_BASE_URL: ' https://admin.example.com ',
|
|
27
28
|
ARCUBASE_ADMIN_TENANT_ID: ' tenant-admin ',
|
|
28
29
|
ARCUBASE_ADMIN_ACCESS_TOKEN: ' token-admin ',
|
|
30
|
+
ARCUBASE_ADMIN_HOST: ' tenant-admin.arcubase.co ',
|
|
29
31
|
ARCUBASE_ADMIN_SUBJECT_TYPE: ' system_admin ',
|
|
30
32
|
ARCUBASE_ADMIN_SUBJECT_ID: ' user-admin ',
|
|
31
33
|
});
|
|
@@ -33,6 +35,7 @@ test('loadRuntimeEnv accepts admin-prefixed runtime env', () => {
|
|
|
33
35
|
ARCUBASE_BASE_URL: 'https://admin.example.com',
|
|
34
36
|
ARCUBASE_TENANT_ID: 'tenant-admin',
|
|
35
37
|
ARCUBASE_ACCESS_TOKEN: 'token-admin',
|
|
38
|
+
ARCUBASE_HOST: 'tenant-admin.arcubase.co',
|
|
36
39
|
ARCUBASE_TRACE_ID: undefined,
|
|
37
40
|
ARCUBASE_SUBJECT_TYPE: 'system_admin',
|
|
38
41
|
ARCUBASE_SUBJECT_ID: 'user-admin',
|
|
@@ -44,6 +47,7 @@ test('loadRuntimeEnv accepts user-prefixed runtime env', () => {
|
|
|
44
47
|
ARCUBASE_USER_BASE_URL: ' https://user.example.com ',
|
|
45
48
|
ARCUBASE_USER_TENANT_ID: ' tenant-user ',
|
|
46
49
|
ARCUBASE_USER_ACCESS_TOKEN: ' token-user ',
|
|
50
|
+
ARCUBASE_USER_HOST: ' tenant-user.arcubase.co ',
|
|
47
51
|
ARCUBASE_USER_SUBJECT_TYPE: ' service_account ',
|
|
48
52
|
ARCUBASE_USER_SUBJECT_ID: ' sa-user ',
|
|
49
53
|
});
|
|
@@ -51,6 +55,7 @@ test('loadRuntimeEnv accepts user-prefixed runtime env', () => {
|
|
|
51
55
|
ARCUBASE_BASE_URL: 'https://user.example.com',
|
|
52
56
|
ARCUBASE_TENANT_ID: 'tenant-user',
|
|
53
57
|
ARCUBASE_ACCESS_TOKEN: 'token-user',
|
|
58
|
+
ARCUBASE_HOST: 'tenant-user.arcubase.co',
|
|
54
59
|
ARCUBASE_TRACE_ID: undefined,
|
|
55
60
|
ARCUBASE_SUBJECT_TYPE: 'service_account',
|
|
56
61
|
ARCUBASE_SUBJECT_ID: 'sa-user',
|
|
@@ -6,9 +6,10 @@ import path from 'path';
|
|
|
6
6
|
import { executeCLI } from '../runtime/execute.js';
|
|
7
7
|
import { CLIError } from '../runtime/errors.js';
|
|
8
8
|
const env = {
|
|
9
|
-
ARCUBASE_BASE_URL: 'https://arcubase.example.com
|
|
9
|
+
ARCUBASE_BASE_URL: 'https://arcubase.example.com',
|
|
10
10
|
ARCUBASE_TENANT_ID: 'tenant_1',
|
|
11
11
|
ARCUBASE_ACCESS_TOKEN: 'tok',
|
|
12
|
+
ARCUBASE_HOST: 'tenant_1.arcubase.co',
|
|
12
13
|
ARCUBASE_TRACE_ID: '',
|
|
13
14
|
ARCUBASE_SUBJECT_TYPE: '',
|
|
14
15
|
ARCUBASE_SUBJECT_ID: '',
|
|
@@ -35,7 +36,14 @@ test('admin save entity rejects invalid body file', async () => {
|
|
|
35
36
|
});
|
|
36
37
|
test('admin create entity executes with valid body file', async () => {
|
|
37
38
|
const bodyFile = tempJSONFile({ name: '订单', parent: 0 });
|
|
38
|
-
const out = await executeCLI('admin', ['app', 'create-entity', '--id', 'app_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 }));
|
|
39
|
+
const out = await executeCLI('admin', ['app', 'create-entity', '--id', 'app_1', '--body-file', bodyFile], env, async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method, host: init?.headers?.['X-Host'] }), { status: 200 }));
|
|
39
40
|
assert.equal(out.kind, 'result');
|
|
40
41
|
assert.equal(out.status, 200);
|
|
42
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entities');
|
|
43
|
+
assert.equal(out.data.host, 'tenant_1.arcubase.co');
|
|
44
|
+
});
|
|
45
|
+
test('admin api-prefixed command executes against external root path', async () => {
|
|
46
|
+
const out = await executeCLI('admin', ['app-entity-share', 'get-app-entity-shares', '--app-id', 'app_1'], env, async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 }));
|
|
47
|
+
assert.equal(out.kind, 'result');
|
|
48
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares');
|
|
41
49
|
});
|
package/dist/tests/help.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { executeCLI } from '../runtime/execute.js';
|
|
4
4
|
const env = {
|
|
5
|
-
ARCUBASE_BASE_URL: 'https://arcubase.example.com
|
|
5
|
+
ARCUBASE_BASE_URL: 'https://arcubase.example.com',
|
|
6
6
|
ARCUBASE_TENANT_ID: 'tenant_1',
|
|
7
7
|
ARCUBASE_ACCESS_TOKEN: 'tok',
|
|
8
8
|
ARCUBASE_TRACE_ID: '',
|
|
@@ -20,3 +20,9 @@ test('module help prints create-entity command', async () => {
|
|
|
20
20
|
assert.equal(out.kind, 'help');
|
|
21
21
|
assert.match(out.text, /create-entity/);
|
|
22
22
|
});
|
|
23
|
+
test('command help prints normalized external endpoint', async () => {
|
|
24
|
+
const out = await executeCLI('admin', ['app-entity-share', 'get-app-entity-shares', '--help'], env, async () => new Response('{}'));
|
|
25
|
+
assert.equal(out.kind, 'help');
|
|
26
|
+
assert.match(out.text, /endpoint: \/apps\/:app_id\/entity-shares/);
|
|
27
|
+
assert.doesNotMatch(out.text, /endpoint: \/api\//);
|
|
28
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carthooks/arcubase-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Arcubase runtime CLI for admin and user command execution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,8 +39,5 @@
|
|
|
39
39
|
"ts-morph": "^25.0.1",
|
|
40
40
|
"tsx": "^4.20.6",
|
|
41
41
|
"typescript": "^5.3.3"
|
|
42
|
-
},
|
|
43
|
-
"publishConfig": {
|
|
44
|
-
"access": "public"
|
|
45
42
|
}
|
|
46
43
|
}
|
package/src/runtime/env.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type RuntimeEnv = {
|
|
|
2
2
|
ARCUBASE_BASE_URL: string
|
|
3
3
|
ARCUBASE_TENANT_ID: string
|
|
4
4
|
ARCUBASE_ACCESS_TOKEN: string
|
|
5
|
+
ARCUBASE_HOST?: string
|
|
5
6
|
ARCUBASE_TRACE_ID?: string
|
|
6
7
|
ARCUBASE_SUBJECT_TYPE?: string
|
|
7
8
|
ARCUBASE_SUBJECT_ID?: string
|
|
@@ -52,6 +53,7 @@ export function loadRuntimeEnv(scope: RuntimeEnvScope, env: EnvInput = process.e
|
|
|
52
53
|
ARCUBASE_BASE_URL: requiredValue(env, scope, 'ARCUBASE_BASE_URL'),
|
|
53
54
|
ARCUBASE_TENANT_ID: requiredValue(env, scope, 'ARCUBASE_TENANT_ID'),
|
|
54
55
|
ARCUBASE_ACCESS_TOKEN: requiredValue(env, scope, 'ARCUBASE_ACCESS_TOKEN'),
|
|
56
|
+
ARCUBASE_HOST: optionalValue(env, scope, 'ARCUBASE_HOST'),
|
|
55
57
|
ARCUBASE_TRACE_ID: optionalValue(env, scope, 'ARCUBASE_TRACE_ID'),
|
|
56
58
|
ARCUBASE_SUBJECT_TYPE: optionalValue(env, scope, 'ARCUBASE_SUBJECT_TYPE'),
|
|
57
59
|
ARCUBASE_SUBJECT_ID: optionalValue(env, scope, 'ARCUBASE_SUBJECT_ID'),
|
package/src/runtime/execute.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseCLIArgs, hasFlag, flagValue } from './argv.js'
|
|
2
2
|
import { loadRuntimeEnv, type RuntimeEnv } from './env.js'
|
|
3
3
|
import { CLIError } from './errors.js'
|
|
4
|
-
import { buildRequestHeaders, buildURL } from './http.js'
|
|
4
|
+
import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http.js'
|
|
5
5
|
import { listModules, listModuleCommands, findCommand, type CommandScope } from './command_registry.js'
|
|
6
6
|
import { loadBodyFromFlags, loadQueryFromFlags } from './body_loader.js'
|
|
7
7
|
import { getBodySchema, getTypeHints, getUnsupportedBodySchemaReason } from './zod_registry.js'
|
|
@@ -47,7 +47,7 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
|
|
|
47
47
|
return [
|
|
48
48
|
`${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} ${commandName}`,
|
|
49
49
|
`method: ${command.method}`,
|
|
50
|
-
`endpoint: ${command.endpoint}`,
|
|
50
|
+
`endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
|
|
51
51
|
command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
|
|
52
52
|
command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
|
|
53
53
|
command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
|
|
@@ -107,7 +107,7 @@ export function summarizeCommand(scope: CommandScope, moduleName: string, comman
|
|
|
107
107
|
scope,
|
|
108
108
|
commandPath: command.commandPath,
|
|
109
109
|
method: command.method,
|
|
110
|
-
endpoint: command.endpoint,
|
|
110
|
+
endpoint: normalizeExternalEndpoint(command.endpoint),
|
|
111
111
|
requestType: command.requestType,
|
|
112
112
|
pathFlags: command.endpointParams,
|
|
113
113
|
scalarFlags: command.scalarParams.map((item) => item.name.replace(/([A-Z])/g, '-$1').toLowerCase()),
|
|
@@ -135,7 +135,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env: Runti
|
|
|
135
135
|
throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
const endpoint = resolveEndpoint(command.endpoint, parsed.flags)
|
|
138
|
+
const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags))
|
|
139
139
|
const scalarQuery = buildScalarQuery(command, parsed.flags)
|
|
140
140
|
const fileQuery = loadQueryFromFlags(parsed.flags)
|
|
141
141
|
const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) }
|
package/src/runtime/http.ts
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import type { RuntimeEnv } from './env.js'
|
|
2
2
|
|
|
3
|
-
export function buildRequestHeaders(env: Pick<RuntimeEnv, 'ARCUBASE_ACCESS_TOKEN' | 'ARCUBASE_TENANT_ID' | 'ARCUBASE_TRACE_ID'>): Record<string, string> {
|
|
3
|
+
export function buildRequestHeaders(env: Pick<RuntimeEnv, 'ARCUBASE_ACCESS_TOKEN' | 'ARCUBASE_TENANT_ID' | 'ARCUBASE_HOST' | 'ARCUBASE_TRACE_ID'>): Record<string, string> {
|
|
4
4
|
const headers: Record<string, string> = {
|
|
5
5
|
Authorization: `Bearer ${env.ARCUBASE_ACCESS_TOKEN}`,
|
|
6
6
|
'X-Tenant-Id': env.ARCUBASE_TENANT_ID
|
|
7
7
|
}
|
|
8
|
+
if (env.ARCUBASE_HOST && env.ARCUBASE_HOST.trim() !== '') {
|
|
9
|
+
headers['X-Host'] = env.ARCUBASE_HOST
|
|
10
|
+
}
|
|
8
11
|
if (env.ARCUBASE_TRACE_ID) {
|
|
9
12
|
headers['X-Trace-Id'] = env.ARCUBASE_TRACE_ID
|
|
10
13
|
}
|
|
11
14
|
return headers
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
export function normalizeExternalEndpoint(endpoint: string): string {
|
|
18
|
+
const trimmed = endpoint.trim()
|
|
19
|
+
if (trimmed === '/api') {
|
|
20
|
+
return '/'
|
|
21
|
+
}
|
|
22
|
+
if (trimmed.startsWith('/api/')) {
|
|
23
|
+
return trimmed.slice(4)
|
|
24
|
+
}
|
|
25
|
+
return trimmed
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
export function buildURL(baseURL: string, endpoint: string, query?: Record<string, any>): string {
|
|
15
|
-
const url = new URL(endpoint, baseURL.endsWith('/') ? baseURL : `${baseURL}/`)
|
|
29
|
+
const url = new URL(normalizeExternalEndpoint(endpoint), baseURL.endsWith('/') ? baseURL : `${baseURL}/`)
|
|
16
30
|
if (query) {
|
|
17
31
|
for (const [key, value] of Object.entries(query)) {
|
|
18
32
|
if (value === undefined || value === null) continue
|
|
@@ -19,6 +19,7 @@ test('loadRuntimeEnv returns normalized runtime env', () => {
|
|
|
19
19
|
ARCUBASE_BASE_URL: 'http://example.com',
|
|
20
20
|
ARCUBASE_TENANT_ID: 'tenant-1',
|
|
21
21
|
ARCUBASE_ACCESS_TOKEN: 'token-1',
|
|
22
|
+
ARCUBASE_HOST: undefined,
|
|
22
23
|
ARCUBASE_TRACE_ID: 'trace-1',
|
|
23
24
|
ARCUBASE_SUBJECT_TYPE: undefined,
|
|
24
25
|
ARCUBASE_SUBJECT_ID: undefined,
|
|
@@ -31,6 +32,7 @@ test('loadRuntimeEnv accepts admin-prefixed runtime env', () => {
|
|
|
31
32
|
ARCUBASE_ADMIN_BASE_URL: ' https://admin.example.com ',
|
|
32
33
|
ARCUBASE_ADMIN_TENANT_ID: ' tenant-admin ',
|
|
33
34
|
ARCUBASE_ADMIN_ACCESS_TOKEN: ' token-admin ',
|
|
35
|
+
ARCUBASE_ADMIN_HOST: ' tenant-admin.arcubase.co ',
|
|
34
36
|
ARCUBASE_ADMIN_SUBJECT_TYPE: ' system_admin ',
|
|
35
37
|
ARCUBASE_ADMIN_SUBJECT_ID: ' user-admin ',
|
|
36
38
|
})
|
|
@@ -39,6 +41,7 @@ test('loadRuntimeEnv accepts admin-prefixed runtime env', () => {
|
|
|
39
41
|
ARCUBASE_BASE_URL: 'https://admin.example.com',
|
|
40
42
|
ARCUBASE_TENANT_ID: 'tenant-admin',
|
|
41
43
|
ARCUBASE_ACCESS_TOKEN: 'token-admin',
|
|
44
|
+
ARCUBASE_HOST: 'tenant-admin.arcubase.co',
|
|
42
45
|
ARCUBASE_TRACE_ID: undefined,
|
|
43
46
|
ARCUBASE_SUBJECT_TYPE: 'system_admin',
|
|
44
47
|
ARCUBASE_SUBJECT_ID: 'user-admin',
|
|
@@ -51,6 +54,7 @@ test('loadRuntimeEnv accepts user-prefixed runtime env', () => {
|
|
|
51
54
|
ARCUBASE_USER_BASE_URL: ' https://user.example.com ',
|
|
52
55
|
ARCUBASE_USER_TENANT_ID: ' tenant-user ',
|
|
53
56
|
ARCUBASE_USER_ACCESS_TOKEN: ' token-user ',
|
|
57
|
+
ARCUBASE_USER_HOST: ' tenant-user.arcubase.co ',
|
|
54
58
|
ARCUBASE_USER_SUBJECT_TYPE: ' service_account ',
|
|
55
59
|
ARCUBASE_USER_SUBJECT_ID: ' sa-user ',
|
|
56
60
|
})
|
|
@@ -59,6 +63,7 @@ test('loadRuntimeEnv accepts user-prefixed runtime env', () => {
|
|
|
59
63
|
ARCUBASE_BASE_URL: 'https://user.example.com',
|
|
60
64
|
ARCUBASE_TENANT_ID: 'tenant-user',
|
|
61
65
|
ARCUBASE_ACCESS_TOKEN: 'token-user',
|
|
66
|
+
ARCUBASE_HOST: 'tenant-user.arcubase.co',
|
|
62
67
|
ARCUBASE_TRACE_ID: undefined,
|
|
63
68
|
ARCUBASE_SUBJECT_TYPE: 'service_account',
|
|
64
69
|
ARCUBASE_SUBJECT_ID: 'sa-user',
|
|
@@ -7,9 +7,10 @@ import { executeCLI } from '../runtime/execute.js'
|
|
|
7
7
|
import { CLIError } from '../runtime/errors.js'
|
|
8
8
|
|
|
9
9
|
const env = {
|
|
10
|
-
ARCUBASE_BASE_URL: 'https://arcubase.example.com
|
|
10
|
+
ARCUBASE_BASE_URL: 'https://arcubase.example.com',
|
|
11
11
|
ARCUBASE_TENANT_ID: 'tenant_1',
|
|
12
12
|
ARCUBASE_ACCESS_TOKEN: 'tok',
|
|
13
|
+
ARCUBASE_HOST: 'tenant_1.arcubase.co',
|
|
13
14
|
ARCUBASE_TRACE_ID: '',
|
|
14
15
|
ARCUBASE_SUBJECT_TYPE: '',
|
|
15
16
|
ARCUBASE_SUBJECT_ID: '',
|
|
@@ -43,8 +44,21 @@ test('admin create entity executes with valid body file', async () => {
|
|
|
43
44
|
'admin',
|
|
44
45
|
['app', 'create-entity', '--id', 'app_1', '--body-file', bodyFile],
|
|
45
46
|
env as any,
|
|
46
|
-
async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 })
|
|
47
|
+
async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method, host: (init?.headers as Record<string, string>)?.['X-Host'] }), { status: 200 })
|
|
47
48
|
)
|
|
48
49
|
assert.equal(out.kind, 'result')
|
|
49
50
|
assert.equal(out.status, 200)
|
|
51
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entities')
|
|
52
|
+
assert.equal(out.data.host, 'tenant_1.arcubase.co')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('admin api-prefixed command executes against external root path', async () => {
|
|
56
|
+
const out = await executeCLI(
|
|
57
|
+
'admin',
|
|
58
|
+
['app-entity-share', 'get-app-entity-shares', '--app-id', 'app_1'],
|
|
59
|
+
env as any,
|
|
60
|
+
async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 })
|
|
61
|
+
)
|
|
62
|
+
assert.equal(out.kind, 'result')
|
|
63
|
+
assert.equal(out.data.url, 'https://arcubase.example.com/apps/app_1/entity-shares')
|
|
50
64
|
})
|
package/src/tests/help.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
import { executeCLI } from '../runtime/execute.js'
|
|
4
4
|
|
|
5
5
|
const env = {
|
|
6
|
-
ARCUBASE_BASE_URL: 'https://arcubase.example.com
|
|
6
|
+
ARCUBASE_BASE_URL: 'https://arcubase.example.com',
|
|
7
7
|
ARCUBASE_TENANT_ID: 'tenant_1',
|
|
8
8
|
ARCUBASE_ACCESS_TOKEN: 'tok',
|
|
9
9
|
ARCUBASE_TRACE_ID: '',
|
|
@@ -23,3 +23,10 @@ test('module help prints create-entity command', async () => {
|
|
|
23
23
|
assert.equal(out.kind, 'help')
|
|
24
24
|
assert.match(out.text, /create-entity/)
|
|
25
25
|
})
|
|
26
|
+
|
|
27
|
+
test('command help prints normalized external endpoint', async () => {
|
|
28
|
+
const out = await executeCLI('admin', ['app-entity-share', 'get-app-entity-shares', '--help'], env as any, async () => new Response('{}'))
|
|
29
|
+
assert.equal(out.kind, 'help')
|
|
30
|
+
assert.match(out.text, /endpoint: \/apps\/:app_id\/entity-shares/)
|
|
31
|
+
assert.doesNotMatch(out.text, /endpoint: \/api\//)
|
|
32
|
+
})
|