@carthooks/arcubase-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/bundle/arcubase-admin.mjs +9875 -0
  2. package/bundle/arcubase.mjs +9875 -0
  3. package/dist/bin/arcubase-admin.d.ts +2 -0
  4. package/dist/bin/arcubase-admin.d.ts.map +1 -0
  5. package/dist/bin/arcubase-admin.js +22 -0
  6. package/dist/bin/arcubase.d.ts +2 -0
  7. package/dist/bin/arcubase.d.ts.map +1 -0
  8. package/dist/bin/arcubase.js +22 -0
  9. package/dist/generated/command_registry.generated.d.ts +3072 -0
  10. package/dist/generated/command_registry.generated.d.ts.map +1 -0
  11. package/dist/generated/command_registry.generated.js +4587 -0
  12. package/dist/generated/type_index.generated.d.ts +496 -0
  13. package/dist/generated/type_index.generated.d.ts.map +1 -0
  14. package/dist/generated/type_index.generated.js +502 -0
  15. package/dist/generated/zod_registry.generated.d.ts +356 -0
  16. package/dist/generated/zod_registry.generated.d.ts.map +1 -0
  17. package/dist/generated/zod_registry.generated.js +483 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +4 -0
  21. package/dist/runtime/argv.d.ts +8 -0
  22. package/dist/runtime/argv.d.ts.map +1 -0
  23. package/dist/runtime/argv.js +31 -0
  24. package/dist/runtime/body_loader.d.ts +3 -0
  25. package/dist/runtime/body_loader.d.ts.map +1 -0
  26. package/dist/runtime/body_loader.js +26 -0
  27. package/dist/runtime/command_registry.d.ts +9 -0
  28. package/dist/runtime/command_registry.d.ts.map +1 -0
  29. package/dist/runtime/command_registry.js +14 -0
  30. package/dist/runtime/env.d.ts +14 -0
  31. package/dist/runtime/env.d.ts.map +1 -0
  32. package/dist/runtime/env.js +42 -0
  33. package/dist/runtime/errors.d.ts +23 -0
  34. package/dist/runtime/errors.d.ts.map +1 -0
  35. package/dist/runtime/errors.js +18 -0
  36. package/dist/runtime/execute.d.ts +17 -0
  37. package/dist/runtime/execute.d.ts.map +1 -0
  38. package/dist/runtime/execute.js +183 -0
  39. package/dist/runtime/http.d.ts +4 -0
  40. package/dist/runtime/http.d.ts.map +1 -0
  41. package/dist/runtime/http.js +21 -0
  42. package/dist/runtime/output.d.ts +2 -0
  43. package/dist/runtime/output.d.ts.map +1 -0
  44. package/dist/runtime/output.js +3 -0
  45. package/dist/runtime/paths.d.ts +4 -0
  46. package/dist/runtime/paths.d.ts.map +1 -0
  47. package/dist/runtime/paths.js +8 -0
  48. package/dist/runtime/zod_registry.d.ts +10 -0
  49. package/dist/runtime/zod_registry.d.ts.map +1 -0
  50. package/dist/runtime/zod_registry.js +50 -0
  51. package/dist/tests/bootstrap.test.d.ts +2 -0
  52. package/dist/tests/bootstrap.test.d.ts.map +1 -0
  53. package/dist/tests/bootstrap.test.js +59 -0
  54. package/dist/tests/command_registry.test.d.ts +2 -0
  55. package/dist/tests/command_registry.test.d.ts.map +1 -0
  56. package/dist/tests/command_registry.test.js +22 -0
  57. package/dist/tests/execute_validation.test.d.ts +2 -0
  58. package/dist/tests/execute_validation.test.d.ts.map +1 -0
  59. package/dist/tests/execute_validation.test.js +41 -0
  60. package/dist/tests/help.test.d.ts +2 -0
  61. package/dist/tests/help.test.d.ts.map +1 -0
  62. package/dist/tests/help.test.js +22 -0
  63. package/dist/tests/zod_registry.test.d.ts +2 -0
  64. package/dist/tests/zod_registry.test.d.ts.map +1 -0
  65. package/dist/tests/zod_registry.test.js +26 -0
  66. package/package.json +46 -0
  67. package/sdk-dist/api/admin/app-entity-share.ts +52 -0
  68. package/sdk-dist/api/admin/app.ts +187 -0
  69. package/sdk-dist/api/admin/automate-rule.ts +59 -0
  70. package/sdk-dist/api/admin/business-hooks.ts +17 -0
  71. package/sdk-dist/api/admin/config.ts +12 -0
  72. package/sdk-dist/api/admin/dashboard.ts +88 -0
  73. package/sdk-dist/api/admin/dataset.ts +133 -0
  74. package/sdk-dist/api/admin/departments.ts +223 -0
  75. package/sdk-dist/api/admin/developer.ts +126 -0
  76. package/sdk-dist/api/admin/entity-tags.ts +71 -0
  77. package/sdk-dist/api/admin/entity.ts +188 -0
  78. package/sdk-dist/api/admin/extension.ts +19 -0
  79. package/sdk-dist/api/admin/index.ts +21 -0
  80. package/sdk-dist/api/admin/ingress.ts +117 -0
  81. package/sdk-dist/api/admin/integration.ts +64 -0
  82. package/sdk-dist/api/admin/kiosk-flow.ts +67 -0
  83. package/sdk-dist/api/admin/print-template-background.ts +43 -0
  84. package/sdk-dist/api/admin/print-templates.ts +60 -0
  85. package/sdk-dist/api/admin/tenant.ts +231 -0
  86. package/sdk-dist/api/admin/workflow.ts +51 -0
  87. package/sdk-dist/api/kiosk/index.ts +4 -0
  88. package/sdk-dist/api/kiosk/starter.ts +34 -0
  89. package/sdk-dist/api/kiosk/ui.ts +79 -0
  90. package/sdk-dist/api/open/api.ts +11 -0
  91. package/sdk-dist/api/open/index.ts +3 -0
  92. package/sdk-dist/api/shared-link/form.ts +103 -0
  93. package/sdk-dist/api/shared-link/index.ts +3 -0
  94. package/sdk-dist/api/user/common.ts +159 -0
  95. package/sdk-dist/api/user/context.ts +10 -0
  96. package/sdk-dist/api/user/copilot.ts +20 -0
  97. package/sdk-dist/api/user/dashboard.ts +43 -0
  98. package/sdk-dist/api/user/entity.ts +213 -0
  99. package/sdk-dist/api/user/flow.ts +20 -0
  100. package/sdk-dist/api/user/global-action.ts +128 -0
  101. package/sdk-dist/api/user/index.ts +12 -0
  102. package/sdk-dist/api/user/kiosk.ts +12 -0
  103. package/sdk-dist/api/user/profile.ts +57 -0
  104. package/sdk-dist/api/user/workflow.ts +186 -0
  105. package/sdk-dist/generated/command_registry.generated.ts +4603 -0
  106. package/sdk-dist/generated/type_index.generated.ts +503 -0
  107. package/sdk-dist/generated/zod_registry.generated.ts +713 -0
  108. package/sdk-dist/types/app-user.ts +0 -0
  109. package/sdk-dist/types/app.ts +182 -0
  110. package/sdk-dist/types/auth.ts +174 -0
  111. package/sdk-dist/types/automate-rule.ts +100 -0
  112. package/sdk-dist/types/business-hooks.ts +37 -0
  113. package/sdk-dist/types/common.ts +676 -0
  114. package/sdk-dist/types/copilot.ts +34 -0
  115. package/sdk-dist/types/dashboard.ts +122 -0
  116. package/sdk-dist/types/dataset.ts +168 -0
  117. package/sdk-dist/types/departments.ts +351 -0
  118. package/sdk-dist/types/developer.ts +190 -0
  119. package/sdk-dist/types/entity.ts +400 -0
  120. package/sdk-dist/types/extension.ts +15 -0
  121. package/sdk-dist/types/global-action.ts +206 -0
  122. package/sdk-dist/types/index.ts +26 -0
  123. package/sdk-dist/types/ingress.ts +173 -0
  124. package/sdk-dist/types/kiosk-starter.ts +46 -0
  125. package/sdk-dist/types/kiosk-ui.ts +94 -0
  126. package/sdk-dist/types/oauth-authorize.ts +52 -0
  127. package/sdk-dist/types/oauth.ts +33 -0
  128. package/sdk-dist/types/openapi.ts +24 -0
  129. package/sdk-dist/types/platform-setup.ts +26 -0
  130. package/sdk-dist/types/shared-link.ts +41 -0
  131. package/sdk-dist/types/tenant.ts +340 -0
  132. package/sdk-dist/types/upload.ts +0 -0
  133. package/sdk-dist/types/user-action.ts +871 -0
  134. package/sdk-dist/types/user-flow.ts +90 -0
  135. package/sdk-dist/types/widgets.ts +115 -0
  136. package/sdk-dist/types/workflow.ts +162 -0
  137. package/src/bin/arcubase-admin.ts +23 -0
  138. package/src/bin/arcubase.ts +23 -0
  139. package/src/generated/command_registry.generated.ts +4603 -0
  140. package/src/generated/type_index.generated.ts +503 -0
  141. package/src/generated/zod_registry.generated.ts +713 -0
  142. package/src/index.ts +4 -0
  143. package/src/runtime/argv.ts +39 -0
  144. package/src/runtime/body_loader.ts +26 -0
  145. package/src/runtime/command_registry.ts +22 -0
  146. package/src/runtime/env.ts +60 -0
  147. package/src/runtime/errors.ts +41 -0
  148. package/src/runtime/execute.ts +206 -0
  149. package/src/runtime/http.ts +23 -0
  150. package/src/runtime/output.ts +3 -0
  151. package/src/runtime/paths.ts +8 -0
  152. package/src/runtime/zod_registry.ts +53 -0
  153. package/src/tests/bootstrap.test.ts +67 -0
  154. package/src/tests/command_registry.test.ts +27 -0
  155. package/src/tests/execute_validation.test.ts +50 -0
  156. package/src/tests/help.test.ts +25 -0
  157. package/src/tests/zod_registry.test.ts +33 -0
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './runtime/env.js'
2
+ export * from './runtime/http.js'
3
+ export * from './runtime/errors.js'
4
+ export * from './runtime/output.js'
@@ -0,0 +1,39 @@
1
+ import { CLIError } from './errors.js'
2
+
3
+ export type ParsedCLIInput = {
4
+ commandTokens: string[]
5
+ flags: Record<string, string | boolean>
6
+ }
7
+
8
+ export function parseCLIArgs(argv: string[]): ParsedCLIInput {
9
+ const commandTokens: string[] = []
10
+ const flags: Record<string, string | boolean> = {}
11
+ for (let i = 0; i < argv.length; i += 1) {
12
+ const token = argv[i]
13
+ if (!token.startsWith('--')) {
14
+ commandTokens.push(token)
15
+ continue
16
+ }
17
+ const name = token.slice(2)
18
+ if (!name) {
19
+ throw new CLIError('INVALID_FLAG', 'flag name is required', 2)
20
+ }
21
+ const next = argv[i + 1]
22
+ if (next && !next.startsWith('--')) {
23
+ flags[name] = next
24
+ i += 1
25
+ continue
26
+ }
27
+ flags[name] = true
28
+ }
29
+ return { commandTokens, flags }
30
+ }
31
+
32
+ export function flagValue(flags: Record<string, string | boolean>, name: string): string | undefined {
33
+ const value = flags[name]
34
+ return typeof value === 'string' ? value : undefined
35
+ }
36
+
37
+ export function hasFlag(flags: Record<string, string | boolean>, name: string): boolean {
38
+ return Boolean(flags[name])
39
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'fs'
2
+ import { CLIError } from './errors.js'
3
+
4
+ function parseJSONFile(filePath: string, label: string): unknown {
5
+ try {
6
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
7
+ } catch (error) {
8
+ throw new CLIError('INVALID_JSON_FILE', `${label} file is invalid: ${filePath}`, 2)
9
+ }
10
+ }
11
+
12
+ export function loadBodyFromFlags(flags: Record<string, string | boolean>): unknown | undefined {
13
+ const bodyFile = typeof flags['body-file'] === 'string' ? flags['body-file'] : undefined
14
+ if (!bodyFile) return undefined
15
+ return parseJSONFile(bodyFile, 'body')
16
+ }
17
+
18
+ export function loadQueryFromFlags(flags: Record<string, string | boolean>): Record<string, any> | undefined {
19
+ const queryFile = typeof flags['query-file'] === 'string' ? flags['query-file'] : undefined
20
+ if (!queryFile) return undefined
21
+ const parsed = parseJSONFile(queryFile, 'query')
22
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
23
+ throw new CLIError('INVALID_QUERY_FILE', 'query file must contain a JSON object', 2)
24
+ }
25
+ return parsed as Record<string, any>
26
+ }
@@ -0,0 +1,22 @@
1
+ import { adminCommands, userCommands } from '../generated/command_registry.generated.js'
2
+
3
+ export { adminCommands, userCommands }
4
+
5
+ export type CommandScope = 'admin' | 'user'
6
+ export type CommandDef = (typeof adminCommands)[number] | (typeof userCommands)[number]
7
+
8
+ export function listCommands(scope: CommandScope): readonly CommandDef[] {
9
+ return scope === 'admin' ? adminCommands : userCommands
10
+ }
11
+
12
+ export function findCommand(scope: CommandScope, moduleName: string, commandName: string): CommandDef | null {
13
+ return listCommands(scope).find((item) => item.commandPath[0] === moduleName && item.commandPath[1] === commandName) ?? null
14
+ }
15
+
16
+ export function listModules(scope: CommandScope): string[] {
17
+ return [...new Set(listCommands(scope).map((item) => item.commandPath[0]))].sort()
18
+ }
19
+
20
+ export function listModuleCommands(scope: CommandScope, moduleName: string): readonly CommandDef[] {
21
+ return listCommands(scope).filter((item) => item.commandPath[0] === moduleName)
22
+ }
@@ -0,0 +1,60 @@
1
+ export type RuntimeEnv = {
2
+ ARCUBASE_BASE_URL: string
3
+ ARCUBASE_TENANT_ID: string
4
+ ARCUBASE_ACCESS_TOKEN: string
5
+ ARCUBASE_TRACE_ID?: string
6
+ ARCUBASE_SUBJECT_TYPE?: string
7
+ ARCUBASE_SUBJECT_ID?: string
8
+ ARCUBASE_TOKEN_EXPIRES_AT?: string
9
+ }
10
+
11
+ export type RuntimeEnvScope = 'admin' | 'user'
12
+
13
+ type EnvInput = Record<string, string | undefined>
14
+
15
+ const PREFIX_BY_SCOPE: Record<RuntimeEnvScope, string> = {
16
+ admin: 'ARCUBASE_ADMIN_',
17
+ user: 'ARCUBASE_USER_',
18
+ }
19
+
20
+ function envValue(env: EnvInput, scope: RuntimeEnvScope, key: keyof RuntimeEnv): string | undefined {
21
+ const direct = env[key]
22
+ if (direct && direct.trim() !== '') {
23
+ return direct.trim()
24
+ }
25
+ const suffix = String(key).replace(/^ARCUBASE_/, '')
26
+ const scopedKey = `${PREFIX_BY_SCOPE[scope]}${suffix}`
27
+ const scopedValue = env[scopedKey]
28
+ if (!scopedValue || scopedValue.trim() === '') {
29
+ return undefined
30
+ }
31
+ return scopedValue.trim()
32
+ }
33
+
34
+ function requiredValue(env: EnvInput, scope: RuntimeEnvScope, key: keyof RuntimeEnv): string {
35
+ const value = envValue(env, scope, key)
36
+ if (!value || value.trim() === '') {
37
+ throw new Error(`${key} is required`)
38
+ }
39
+ return value
40
+ }
41
+
42
+ function optionalValue(env: EnvInput, scope: RuntimeEnvScope, key: keyof RuntimeEnv): string | undefined {
43
+ const value = envValue(env, scope, key)
44
+ if (!value || value.trim() === '') {
45
+ return undefined
46
+ }
47
+ return value
48
+ }
49
+
50
+ export function loadRuntimeEnv(scope: RuntimeEnvScope, env: EnvInput = process.env): RuntimeEnv {
51
+ return {
52
+ ARCUBASE_BASE_URL: requiredValue(env, scope, 'ARCUBASE_BASE_URL'),
53
+ ARCUBASE_TENANT_ID: requiredValue(env, scope, 'ARCUBASE_TENANT_ID'),
54
+ ARCUBASE_ACCESS_TOKEN: requiredValue(env, scope, 'ARCUBASE_ACCESS_TOKEN'),
55
+ ARCUBASE_TRACE_ID: optionalValue(env, scope, 'ARCUBASE_TRACE_ID'),
56
+ ARCUBASE_SUBJECT_TYPE: optionalValue(env, scope, 'ARCUBASE_SUBJECT_TYPE'),
57
+ ARCUBASE_SUBJECT_ID: optionalValue(env, scope, 'ARCUBASE_SUBJECT_ID'),
58
+ ARCUBASE_TOKEN_EXPIRES_AT: optionalValue(env, scope, 'ARCUBASE_TOKEN_EXPIRES_AT')
59
+ }
60
+ }
@@ -0,0 +1,41 @@
1
+ export type CLIValidationIssue = {
2
+ path: string
3
+ message: string
4
+ }
5
+
6
+ export type CLITSTypeHint = {
7
+ symbol: string
8
+ file: string
9
+ }
10
+
11
+ export type CLIErrorDetails = {
12
+ operation?: string
13
+ requestType?: string
14
+ issues?: CLIValidationIssue[]
15
+ tsHints?: CLITSTypeHint[]
16
+ reason?: string
17
+ }
18
+
19
+ export class CLIError extends Error {
20
+ readonly code: string
21
+ readonly exitCode: number
22
+ readonly details?: CLIErrorDetails
23
+
24
+ constructor(code: string, message: string, exitCode = 1, details?: CLIErrorDetails) {
25
+ super(message)
26
+ this.name = 'CLIError'
27
+ this.code = code
28
+ this.exitCode = exitCode
29
+ this.details = details
30
+ }
31
+
32
+ toJSON(): Record<string, unknown> {
33
+ return {
34
+ error: {
35
+ code: this.code,
36
+ message: this.message,
37
+ ...(this.details ? this.details : {}),
38
+ },
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,206 @@
1
+ import { parseCLIArgs, hasFlag, flagValue } from './argv.js'
2
+ import { loadRuntimeEnv, type RuntimeEnv } from './env.js'
3
+ import { CLIError } from './errors.js'
4
+ import { buildRequestHeaders, buildURL } from './http.js'
5
+ import { listModules, listModuleCommands, findCommand, type CommandScope } from './command_registry.js'
6
+ import { loadBodyFromFlags, loadQueryFromFlags } from './body_loader.js'
7
+ import { getBodySchema, getTypeHints, getUnsupportedBodySchemaReason } from './zod_registry.js'
8
+
9
+ export type CommandExecutionSummary = {
10
+ scope: CommandScope
11
+ commandPath: readonly [string, string]
12
+ method: string
13
+ endpoint: string
14
+ requestType: string | null
15
+ pathFlags: readonly string[]
16
+ scalarFlags: readonly string[]
17
+ }
18
+
19
+ export function renderRootHelp(scope: CommandScope): string {
20
+ const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase'
21
+ return [
22
+ `${binary} <module> <command> [flags]`,
23
+ '',
24
+ 'modules:',
25
+ ...listModules(scope).map((item) => ` - ${item}`),
26
+ ].join('\n')
27
+ }
28
+
29
+ export function renderModuleHelp(scope: CommandScope, moduleName: string): string {
30
+ const items = listModuleCommands(scope, moduleName)
31
+ if (items.length === 0) {
32
+ throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2)
33
+ }
34
+ return [
35
+ `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
36
+ '',
37
+ 'commands:',
38
+ ...items.map((item) => ` - ${item.commandPath[1]}`),
39
+ ].join('\n')
40
+ }
41
+
42
+ export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName: string): string {
43
+ const command = findCommand(scope, moduleName, commandName)
44
+ if (!command) {
45
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
46
+ }
47
+ return [
48
+ `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} ${commandName}`,
49
+ `method: ${command.method}`,
50
+ `endpoint: ${command.endpoint}`,
51
+ command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
52
+ command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
53
+ command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
54
+ ].join('\n')
55
+ }
56
+
57
+ function coerceScalar(value: string, typeText: string): any {
58
+ const normalized = typeText.trim()
59
+ if (normalized === 'number' || normalized === 'string | number') {
60
+ const parsed = Number(value)
61
+ if (Number.isNaN(parsed)) {
62
+ throw new CLIError('INVALID_FLAG_VALUE', `expected number, got ${value}`, 2)
63
+ }
64
+ return parsed
65
+ }
66
+ if (normalized === 'boolean') {
67
+ if (value === 'true') return true
68
+ if (value === 'false') return false
69
+ throw new CLIError('INVALID_FLAG_VALUE', `expected boolean, got ${value}`, 2)
70
+ }
71
+ return value
72
+ }
73
+
74
+ function resolveEndpoint(endpoint: string, flags: Record<string, string | boolean>): string {
75
+ let resolved = endpoint
76
+ for (const placeholder of [...endpoint.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1])) {
77
+ const flagName = placeholder.replace(/_/g, '-')
78
+ const value = flagValue(flags, flagName)
79
+ if (!value) {
80
+ throw new CLIError('MISSING_PATH_FLAG', `missing required flag --${flagName}`, 2)
81
+ }
82
+ resolved = resolved.replace(`:${placeholder}`, encodeURIComponent(value))
83
+ }
84
+ return resolved
85
+ }
86
+
87
+ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record<string, string | boolean>): Record<string, any> | undefined {
88
+ if (!command) {
89
+ return undefined
90
+ }
91
+ const out: Record<string, any> = {}
92
+ for (const scalar of command.scalarParams) {
93
+ const flagName = scalar.name.replace(/([A-Z])/g, '-$1').toLowerCase()
94
+ const value = flagValue(flags, flagName)
95
+ if (value === undefined) continue
96
+ out[scalar.name] = coerceScalar(value, scalar.type)
97
+ }
98
+ return Object.keys(out).length > 0 ? out : undefined
99
+ }
100
+
101
+ export function summarizeCommand(scope: CommandScope, moduleName: string, commandName: string): CommandExecutionSummary {
102
+ const command = findCommand(scope, moduleName, commandName)
103
+ if (!command) {
104
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
105
+ }
106
+ return {
107
+ scope,
108
+ commandPath: command.commandPath,
109
+ method: command.method,
110
+ endpoint: command.endpoint,
111
+ requestType: command.requestType,
112
+ pathFlags: command.endpointParams,
113
+ scalarFlags: command.scalarParams.map((item) => item.name.replace(/([A-Z])/g, '-$1').toLowerCase()),
114
+ }
115
+ }
116
+
117
+ export async function executeCLI(scope: CommandScope, argv: string[], env: RuntimeEnv = loadRuntimeEnv(scope), fetchImpl: typeof fetch = fetch): Promise<any> {
118
+ const parsed = parseCLIArgs(argv)
119
+ if (parsed.commandTokens.length === 0) {
120
+ return { kind: 'help', text: renderRootHelp(scope) }
121
+ }
122
+ const [moduleName, commandName] = parsed.commandTokens
123
+ if (!moduleName) {
124
+ return { kind: 'help', text: renderRootHelp(scope) }
125
+ }
126
+ if (!commandName) {
127
+ return { kind: 'help', text: renderModuleHelp(scope, moduleName) }
128
+ }
129
+ if (hasFlag(parsed.flags, 'help')) {
130
+ return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName) }
131
+ }
132
+
133
+ const command = findCommand(scope, moduleName, commandName)
134
+ if (!command) {
135
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2)
136
+ }
137
+
138
+ const endpoint = resolveEndpoint(command.endpoint, parsed.flags)
139
+ const scalarQuery = buildScalarQuery(command, parsed.flags)
140
+ const fileQuery = loadQueryFromFlags(parsed.flags)
141
+ const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) }
142
+ const body = loadBodyFromFlags(parsed.flags)
143
+
144
+ if (command.requestType) {
145
+ if (body === undefined) {
146
+ throw new CLIError('MISSING_BODY_FILE', `command requires --body-file for ${command.requestType}`, 2)
147
+ }
148
+ const schema = getBodySchema(command.requestType)
149
+ if (schema) {
150
+ const parsedBody = schema.safeParse(body)
151
+ if (!parsedBody.success) {
152
+ throw new CLIError(
153
+ 'BODY_VALIDATION_FAILED',
154
+ parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; '),
155
+ 2,
156
+ {
157
+ operation: `${scope}.${command.module}.${command.functionName}`,
158
+ requestType: command.requestType,
159
+ issues: parsedBody.error.issues.map((issue) => ({
160
+ path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
161
+ message: issue.message,
162
+ })),
163
+ tsHints: getTypeHints(command.requestType),
164
+ }
165
+ )
166
+ }
167
+ } else {
168
+ const unsupportedReason = getUnsupportedBodySchemaReason(command.requestType)
169
+ if (unsupportedReason) {
170
+ throw new CLIError('UNSUPPORTED_BODY_SCHEMA', `${command.requestType}: ${unsupportedReason}`, 2, {
171
+ operation: `${scope}.${command.module}.${command.functionName}`,
172
+ requestType: command.requestType,
173
+ reason: unsupportedReason,
174
+ tsHints: getTypeHints(command.requestType),
175
+ })
176
+ }
177
+ }
178
+ }
179
+
180
+ const headers = buildRequestHeaders(env)
181
+ const response = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
182
+ method: command.method,
183
+ headers: {
184
+ ...headers,
185
+ 'Content-Type': 'application/json',
186
+ },
187
+ body: body === undefined || command.method === 'GET' ? undefined : JSON.stringify(body),
188
+ })
189
+
190
+ const raw = await response.text()
191
+ let parsedResponse: any = raw
192
+ try {
193
+ parsedResponse = raw ? JSON.parse(raw) : null
194
+ } catch {
195
+ // keep raw text
196
+ }
197
+ if (!response.ok) {
198
+ throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1)
199
+ }
200
+ return {
201
+ kind: 'result',
202
+ commandPath: command.commandPath,
203
+ status: response.status,
204
+ data: parsedResponse,
205
+ }
206
+ }
@@ -0,0 +1,23 @@
1
+ import type { RuntimeEnv } from './env.js'
2
+
3
+ export function buildRequestHeaders(env: Pick<RuntimeEnv, 'ARCUBASE_ACCESS_TOKEN' | 'ARCUBASE_TENANT_ID' | 'ARCUBASE_TRACE_ID'>): Record<string, string> {
4
+ const headers: Record<string, string> = {
5
+ Authorization: `Bearer ${env.ARCUBASE_ACCESS_TOKEN}`,
6
+ 'X-Tenant-Id': env.ARCUBASE_TENANT_ID
7
+ }
8
+ if (env.ARCUBASE_TRACE_ID) {
9
+ headers['X-Trace-Id'] = env.ARCUBASE_TRACE_ID
10
+ }
11
+ return headers
12
+ }
13
+
14
+ export function buildURL(baseURL: string, endpoint: string, query?: Record<string, any>): string {
15
+ const url = new URL(endpoint, baseURL.endsWith('/') ? baseURL : `${baseURL}/`)
16
+ if (query) {
17
+ for (const [key, value] of Object.entries(query)) {
18
+ if (value === undefined || value === null) continue
19
+ url.searchParams.set(key, typeof value === 'string' ? value : JSON.stringify(value))
20
+ }
21
+ }
22
+ return url.toString()
23
+ }
@@ -0,0 +1,3 @@
1
+ export function writeJSON(value: unknown): void {
2
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
3
+ }
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_ARCUBASE_CLI_ROOT = '/opt/arcubase-cli'
2
+ export const DEFAULT_ARCUBASE_SDK_ROOT = '/opt/arcubase-sdk'
3
+
4
+ export function resolveArcubaseSDKRoot(env: Record<string, string | undefined> = process.env): string {
5
+ const value = env.ARCUBASE_SDK_ROOT?.trim()
6
+ if (value) return value
7
+ return DEFAULT_ARCUBASE_SDK_ROOT
8
+ }
@@ -0,0 +1,53 @@
1
+ import type { ZodTypeAny } from 'zod'
2
+ import { bodySchemas, unsupportedBodySchemas } from '../generated/zod_registry.generated.js'
3
+ import { typeIndex } from '../generated/type_index.generated.js'
4
+
5
+ export function getBodySchema(typeName: string): ZodTypeAny | null {
6
+ const name = String(typeName || '').trim()
7
+ if (!name) return null
8
+ return (bodySchemas as Record<string, ZodTypeAny | undefined>)[name] ?? null
9
+ }
10
+
11
+ export function getUnsupportedBodySchemaReason(typeName: string): string | null {
12
+ const name = String(typeName || '').trim()
13
+ if (!name) return null
14
+ return (unsupportedBodySchemas as Record<string, string | undefined>)[name] ?? null
15
+ }
16
+
17
+ export function requireBodySchema(typeName: string): ZodTypeAny {
18
+ const schema = getBodySchema(typeName)
19
+ if (schema) return schema
20
+ const reason = getUnsupportedBodySchemaReason(typeName)
21
+ if (reason) {
22
+ throw new Error(`runtime validation for ${typeName} is not supported: ${reason}`)
23
+ }
24
+ throw new Error(`runtime validation schema not found for ${typeName}`)
25
+ }
26
+
27
+ export type RuntimeTypeHint = {
28
+ symbol: string
29
+ file: string
30
+ }
31
+
32
+ export function getTypeHints(typeName: string): RuntimeTypeHint[] {
33
+ const name = String(typeName || '').trim()
34
+ if (!name) return []
35
+ const entry = (typeIndex as Record<string, { file: string; dependencies?: ReadonlyArray<RuntimeTypeHint> } | undefined>)[name]
36
+ if (!entry?.file) return []
37
+ const seen = new Set<string>()
38
+ const out: RuntimeTypeHint[] = []
39
+ const append = (symbol: string, file: string) => {
40
+ symbol = String(symbol || '').trim()
41
+ file = String(file || '').trim()
42
+ if (!symbol || !file) return
43
+ const key = `${symbol}@@${file}`
44
+ if (seen.has(key)) return
45
+ seen.add(key)
46
+ out.push({ symbol, file })
47
+ }
48
+ append(name, entry.file)
49
+ for (const dep of entry.dependencies ?? []) {
50
+ append(dep.symbol, dep.file)
51
+ }
52
+ return out
53
+ }
@@ -0,0 +1,67 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { loadRuntimeEnv } from '../runtime/env.js'
5
+
6
+ test('loadRuntimeEnv requires base url, tenant id and access token', () => {
7
+ assert.throws(() => loadRuntimeEnv('admin', {}), /ARCUBASE_BASE_URL is required/)
8
+ })
9
+
10
+ test('loadRuntimeEnv returns normalized runtime env', () => {
11
+ const env = loadRuntimeEnv('admin', {
12
+ ARCUBASE_BASE_URL: ' http://example.com ',
13
+ ARCUBASE_TENANT_ID: ' tenant-1 ',
14
+ ARCUBASE_ACCESS_TOKEN: ' token-1 ',
15
+ ARCUBASE_TRACE_ID: ' trace-1 '
16
+ })
17
+
18
+ assert.deepEqual(env, {
19
+ ARCUBASE_BASE_URL: 'http://example.com',
20
+ ARCUBASE_TENANT_ID: 'tenant-1',
21
+ ARCUBASE_ACCESS_TOKEN: 'token-1',
22
+ ARCUBASE_TRACE_ID: 'trace-1',
23
+ ARCUBASE_SUBJECT_TYPE: undefined,
24
+ ARCUBASE_SUBJECT_ID: undefined,
25
+ ARCUBASE_TOKEN_EXPIRES_AT: undefined
26
+ })
27
+ })
28
+
29
+ test('loadRuntimeEnv accepts admin-prefixed runtime env', () => {
30
+ const env = loadRuntimeEnv('admin', {
31
+ ARCUBASE_ADMIN_BASE_URL: ' https://admin.example.com ',
32
+ ARCUBASE_ADMIN_TENANT_ID: ' tenant-admin ',
33
+ ARCUBASE_ADMIN_ACCESS_TOKEN: ' token-admin ',
34
+ ARCUBASE_ADMIN_SUBJECT_TYPE: ' system_admin ',
35
+ ARCUBASE_ADMIN_SUBJECT_ID: ' user-admin ',
36
+ })
37
+
38
+ assert.deepEqual(env, {
39
+ ARCUBASE_BASE_URL: 'https://admin.example.com',
40
+ ARCUBASE_TENANT_ID: 'tenant-admin',
41
+ ARCUBASE_ACCESS_TOKEN: 'token-admin',
42
+ ARCUBASE_TRACE_ID: undefined,
43
+ ARCUBASE_SUBJECT_TYPE: 'system_admin',
44
+ ARCUBASE_SUBJECT_ID: 'user-admin',
45
+ ARCUBASE_TOKEN_EXPIRES_AT: undefined
46
+ })
47
+ })
48
+
49
+ test('loadRuntimeEnv accepts user-prefixed runtime env', () => {
50
+ const env = loadRuntimeEnv('user', {
51
+ ARCUBASE_USER_BASE_URL: ' https://user.example.com ',
52
+ ARCUBASE_USER_TENANT_ID: ' tenant-user ',
53
+ ARCUBASE_USER_ACCESS_TOKEN: ' token-user ',
54
+ ARCUBASE_USER_SUBJECT_TYPE: ' service_account ',
55
+ ARCUBASE_USER_SUBJECT_ID: ' sa-user ',
56
+ })
57
+
58
+ assert.deepEqual(env, {
59
+ ARCUBASE_BASE_URL: 'https://user.example.com',
60
+ ARCUBASE_TENANT_ID: 'tenant-user',
61
+ ARCUBASE_ACCESS_TOKEN: 'token-user',
62
+ ARCUBASE_TRACE_ID: undefined,
63
+ ARCUBASE_SUBJECT_TYPE: 'service_account',
64
+ ARCUBASE_SUBJECT_ID: 'sa-user',
65
+ ARCUBASE_TOKEN_EXPIRES_AT: undefined
66
+ })
67
+ })
@@ -0,0 +1,27 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import { adminCommands, userCommands } from '../generated/command_registry.generated.js'
5
+
6
+ test('admin app create-entity command exists', () => {
7
+ assert.ok(adminCommands.some((c) => c.commandPath.join(' ') === 'app create-entity'))
8
+ })
9
+
10
+ test('user workflow query-entity command exists', () => {
11
+ assert.ok(userCommands.some((c) => c.commandPath.join(' ') === 'workflow query-entity'))
12
+ })
13
+
14
+ test('inline request type keeps full object text instead of truncated brace', () => {
15
+ const command = adminCommands.find((c) => c.commandPath.join(' ') === 'dashboard clone-dashboard')
16
+ assert.ok(command)
17
+ const typed = command!
18
+ assert.match(String(typed.requestType), /^\{\s*name:\s*string\s*\}$/)
19
+ })
20
+
21
+ test('tag commands no longer emit truncated inline request type', () => {
22
+ const command = adminCommands.find((c) => c.commandPath.join(' ') === 'entity-tags batch-add-entity-tags')
23
+ assert.ok(command)
24
+ const typed = command!
25
+ assert.match(String(typed.requestType), /^\{\s*policy_id:\s*string.*selection:\s*any.*tag_names:\s*string\[\]\s*\}$/)
26
+ assert.equal(typed.scalarParams.length, 0)
27
+ })
@@ -0,0 +1,50 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'fs'
4
+ import os from 'os'
5
+ import path from 'path'
6
+ import { executeCLI } from '../runtime/execute.js'
7
+ import { CLIError } from '../runtime/errors.js'
8
+
9
+ const env = {
10
+ ARCUBASE_BASE_URL: 'https://arcubase.example.com/api/',
11
+ ARCUBASE_TENANT_ID: 'tenant_1',
12
+ ARCUBASE_ACCESS_TOKEN: 'tok',
13
+ ARCUBASE_TRACE_ID: '',
14
+ ARCUBASE_SUBJECT_TYPE: '',
15
+ ARCUBASE_SUBJECT_ID: '',
16
+ ARCUBASE_TOKEN_EXPIRES_AT: '',
17
+ }
18
+
19
+ function tempJSONFile(payload: unknown): string {
20
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'arcubase-cli-'))
21
+ const filePath = path.join(dir, 'payload.json')
22
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2))
23
+ return filePath
24
+ }
25
+
26
+ test('admin save entity rejects invalid body file', async () => {
27
+ const invalidFile = tempJSONFile({ name: '订单', layout: 'bad-layout' })
28
+ await assert.rejects(async () => {
29
+ await executeCLI('admin', ['entity', 'admin-save-entity', '--app-id', 'app_1', '--entity-id', 'entity_1', '--body-file', invalidFile], env as any, async () => new Response('{}'))
30
+ }, (error: unknown) => {
31
+ assert.ok(error instanceof CLIError)
32
+ assert.equal(error.code, 'BODY_VALIDATION_FAILED')
33
+ assert.equal(error.details?.requestType, 'EntitySaveReqVO')
34
+ assert.ok((error.details?.issues ?? []).some((item) => item.path.startsWith('body.layout')))
35
+ assert.ok((error.details?.tsHints ?? []).some((item) => item.file.startsWith('/opt/arcubase-sdk/')))
36
+ return true
37
+ })
38
+ })
39
+
40
+ test('admin create entity executes with valid body file', async () => {
41
+ const bodyFile = tempJSONFile({ name: '订单', parent: 0 })
42
+ const out = await executeCLI(
43
+ 'admin',
44
+ ['app', 'create-entity', '--id', 'app_1', '--body-file', bodyFile],
45
+ env as any,
46
+ async (url, init) => new Response(JSON.stringify({ ok: true, url, method: init?.method }), { status: 200 })
47
+ )
48
+ assert.equal(out.kind, 'result')
49
+ assert.equal(out.status, 200)
50
+ })
@@ -0,0 +1,25 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { executeCLI } from '../runtime/execute.js'
4
+
5
+ const env = {
6
+ ARCUBASE_BASE_URL: 'https://arcubase.example.com/api/',
7
+ ARCUBASE_TENANT_ID: 'tenant_1',
8
+ ARCUBASE_ACCESS_TOKEN: 'tok',
9
+ ARCUBASE_TRACE_ID: '',
10
+ ARCUBASE_SUBJECT_TYPE: '',
11
+ ARCUBASE_SUBJECT_ID: '',
12
+ ARCUBASE_TOKEN_EXPIRES_AT: '',
13
+ }
14
+
15
+ test('root help prints modules', async () => {
16
+ const out = await executeCLI('admin', ['--help'], env as any, async () => new Response('{}'))
17
+ assert.equal(out.kind, 'help')
18
+ assert.match(out.text, /app/)
19
+ })
20
+
21
+ test('module help prints create-entity command', async () => {
22
+ const out = await executeCLI('admin', ['app', '--help'], env as any, async () => new Response('{}'))
23
+ assert.equal(out.kind, 'help')
24
+ assert.match(out.text, /create-entity/)
25
+ })