@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.
@@ -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 ?? {} };
@@ -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 ?? {} };
@@ -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;
@@ -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,CAU9F"}
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"}
@@ -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'),
@@ -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 ?? {}) };
@@ -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,CASvJ;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"}
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"}
@@ -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/api/',
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
  });
@@ -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/api/',
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.0",
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
  }
@@ -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'),
@@ -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 ?? {}) }
@@ -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/api/',
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
  })
@@ -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/api/',
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
+ })