@carthooks/arcubase-cli 0.1.7 → 0.1.9

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 (66) hide show
  1. package/bundle/arcubase-admin.mjs +1042 -5056
  2. package/bundle/arcubase.mjs +1042 -5056
  3. package/dist/generated/command_registry.generated.d.ts +693 -2812
  4. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  5. package/dist/generated/command_registry.generated.js +812 -4100
  6. package/dist/generated/type_index.generated.d.ts +8 -397
  7. package/dist/generated/type_index.generated.d.ts.map +1 -1
  8. package/dist/generated/type_index.generated.js +8 -403
  9. package/dist/generated/zod_registry.generated.d.ts +33 -261
  10. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  11. package/dist/generated/zod_registry.generated.js +36 -365
  12. package/dist/runtime/body_loader.d.ts.map +1 -1
  13. package/dist/runtime/body_loader.js +12 -0
  14. package/dist/runtime/command_registry.d.ts +3 -21
  15. package/dist/runtime/command_registry.d.ts.map +1 -1
  16. package/dist/runtime/command_registry.js +17 -23
  17. package/dist/runtime/execute.d.ts +3 -3
  18. package/dist/runtime/execute.d.ts.map +1 -1
  19. package/dist/runtime/execute.js +53 -157
  20. package/dist/runtime/http.js +1 -1
  21. package/dist/runtime/upload.js +3 -3
  22. package/dist/runtime/zod_registry.d.ts.map +1 -1
  23. package/dist/runtime/zod_registry.js +7 -24
  24. package/dist/tests/command_registry.test.js +81 -28
  25. package/dist/tests/execute_validation.test.js +175 -510
  26. package/dist/tests/help.test.js +39 -62
  27. package/dist/tests/upload.test.js +4 -4
  28. package/dist/tests/zod_registry.test.js +0 -10
  29. package/package.json +1 -1
  30. package/sdk-dist/docs/runtime-reference/README.md +20 -30
  31. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +4 -5
  32. package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
  33. package/sdk-dist/docs/runtime-reference/entity-schema/query.md +3 -3
  34. package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +3 -3
  35. package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +3 -3
  36. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +2 -2
  37. package/sdk-dist/docs/runtime-reference/entity-schema.md +4 -4
  38. package/sdk-dist/docs/runtime-reference/examples/README.md +2 -2
  39. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +12 -12
  40. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +1 -1
  41. package/sdk-dist/docs/runtime-reference/row-crud.md +11 -32
  42. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +16 -47
  43. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +11 -27
  44. package/sdk-dist/docs/runtime-reference/uploads.md +1 -1
  45. package/sdk-dist/docs/runtime-reference/workflow/README.md +19 -0
  46. package/sdk-dist/generated/command_registry.generated.ts +814 -4102
  47. package/sdk-dist/generated/type_index.generated.ts +8 -403
  48. package/sdk-dist/generated/zod_registry.generated.ts +44 -500
  49. package/sdk-dist/types/app.ts +3 -10
  50. package/sdk-dist/types/common.ts +7 -0
  51. package/sdk-dist/types/global-action.ts +2 -1
  52. package/src/generated/command_registry.generated.ts +814 -4102
  53. package/src/generated/type_index.generated.ts +8 -403
  54. package/src/generated/zod_registry.generated.ts +44 -500
  55. package/src/runtime/body_loader.ts +11 -0
  56. package/src/runtime/command_registry.ts +22 -44
  57. package/src/runtime/execute.ts +157 -165
  58. package/src/runtime/http.ts +1 -1
  59. package/src/runtime/upload.ts +3 -3
  60. package/src/runtime/zod_registry.ts +7 -25
  61. package/src/tests/command_registry.test.ts +81 -31
  62. package/src/tests/execute_validation.test.ts +236 -603
  63. package/src/tests/help.test.ts +47 -66
  64. package/src/tests/upload.test.ts +4 -4
  65. package/src/tests/zod_registry.test.ts +0 -13
  66. package/sdk-dist/docs/runtime-reference/app-discovery.md +0 -68
@@ -11,6 +11,17 @@ function parseJSONFile(filePath: string, label: string): unknown {
11
11
 
12
12
  export function loadBodyFromFlags(flags: Record<string, string | boolean>): unknown | undefined {
13
13
  const bodyFile = typeof flags['body-file'] === 'string' ? flags['body-file'] : undefined
14
+ const bodyJSON = typeof flags['body-json'] === 'string' ? flags['body-json'] : undefined
15
+ if (bodyFile && bodyJSON) {
16
+ throw new CLIError('BODY_JSON_CONFLICT', 'use either --body-file or --body-json, not both', 2)
17
+ }
18
+ if (bodyJSON !== undefined) {
19
+ try {
20
+ return JSON.parse(bodyJSON)
21
+ } catch {
22
+ throw new CLIError('INVALID_BODY_JSON', 'body json is invalid', 2)
23
+ }
24
+ }
14
25
  if (!bodyFile) return undefined
15
26
  return parseJSONFile(bodyFile, 'body')
16
27
  }
@@ -3,50 +3,22 @@ import { adminCommands, userCommands } from '../generated/command_registry.gener
3
3
  export { adminCommands, userCommands }
4
4
 
5
5
  export type CommandScope = 'admin' | 'user'
6
- type GeneratedCommandDef = (typeof adminCommands)[number] | (typeof userCommands)[number]
7
-
8
- type CustomCommandDef = {
9
- scope: CommandScope
10
- module: string
11
- functionName: string
12
- commandPath: readonly [string, string]
13
- method: string
14
- endpoint: string
15
- endpointParams: readonly string[]
16
- controller: string
17
- requestType: string | null
18
- scalarParams: readonly { name: string; type: string; hasDefault: boolean }[]
19
- responseType: string
20
- }
21
-
22
- export type CommandDef = GeneratedCommandDef | CustomCommandDef
23
-
24
- const customAdminCommands: readonly CustomCommandDef[] = [
25
- {
26
- scope: 'admin',
27
- module: 'app',
28
- functionName: 'inventory',
29
- commandPath: ['app', 'inventory'],
30
- method: 'GET',
31
- endpoint: '/apps',
32
- endpointParams: [],
33
- controller: 'Runtime.AppInventory',
34
- requestType: null,
35
- scalarParams: [],
36
- responseType: 'AppInventoryRespVO',
37
- },
38
- ] as const
39
-
40
- const customUserCommands: readonly CustomCommandDef[] = []
6
+ export type CommandDef = (typeof adminCommands)[number] | (typeof userCommands)[number]
41
7
 
42
8
  export function listCommands(scope: CommandScope): readonly CommandDef[] {
43
- return scope === 'admin'
44
- ? [...adminCommands, ...customAdminCommands]
45
- : [...userCommands, ...customUserCommands]
9
+ return scope === 'admin' ? adminCommands : userCommands
46
10
  }
47
11
 
48
- export function findCommand(scope: CommandScope, moduleName: string, commandName: string): CommandDef | null {
49
- return listCommands(scope).find((item) => item.commandPath[0] === moduleName && item.commandPath[1] === commandName) ?? null
12
+ export function findCommand(scope: CommandScope, moduleName: string, commandName?: string | null): CommandDef | null {
13
+ return listCommands(scope).find((item) => {
14
+ if (item.commandPath[0] !== moduleName) {
15
+ return false
16
+ }
17
+ if (item.commandPath.length === 1) {
18
+ return !commandName
19
+ }
20
+ return item.commandPath[1] === commandName
21
+ }) ?? null
50
22
  }
51
23
 
52
24
  export function listModules(scope: CommandScope): string[] {
@@ -77,7 +49,7 @@ function levenshtein(a: string, b: string): number {
77
49
  export type CommandSuggestion = {
78
50
  scope: CommandScope
79
51
  moduleName: string
80
- commandName: string
52
+ commandName: string | null
81
53
  binary: 'arcubase-admin' | 'arcubase'
82
54
  score: number
83
55
  }
@@ -86,9 +58,10 @@ export function findCommandSuggestions(moduleName: string, commandName: string,
86
58
  const suggestions: CommandSuggestion[] = []
87
59
  for (const scope of ['admin', 'user'] as const) {
88
60
  for (const command of listCommands(scope)) {
89
- const [candidateModule, candidateCommand] = command.commandPath
61
+ const candidateModule = command.commandPath[0]
62
+ const candidateCommand: string | null = command.commandPath.length > 1 ? command.commandPath[1] ?? null : null
90
63
  const moduleScore = candidateModule === moduleName ? 0 : levenshtein(candidateModule, moduleName || '')
91
- const commandScore = candidateCommand === commandName ? 0 : levenshtein(candidateCommand, commandName || '')
64
+ const commandScore = candidateCommand === commandName ? 0 : levenshtein(candidateCommand ?? '', commandName || '')
92
65
  const score = commandScore * 3 + moduleScore
93
66
  suggestions.push({
94
67
  scope,
@@ -100,6 +73,11 @@ export function findCommandSuggestions(moduleName: string, commandName: string,
100
73
  }
101
74
  }
102
75
  return suggestions
103
- .sort((a, b) => a.score - b.score || a.binary.localeCompare(b.binary) || a.moduleName.localeCompare(b.moduleName) || a.commandName.localeCompare(b.commandName))
76
+ .sort((a, b) =>
77
+ a.score - b.score ||
78
+ a.binary.localeCompare(b.binary) ||
79
+ a.moduleName.localeCompare(b.moduleName) ||
80
+ (a.commandName ?? '').localeCompare(b.commandName ?? ''),
81
+ )
104
82
  .slice(0, limit)
105
83
  }
@@ -9,12 +9,12 @@ import { renderUploadHelp, uploadLocalFile } from './upload.js'
9
9
 
10
10
  export type CommandExecutionSummary = {
11
11
  scope: CommandScope
12
- commandPath: readonly [string, string]
12
+ commandPath: readonly string[]
13
13
  method: string
14
14
  endpoint: string
15
15
  requestType: string | null
16
16
  pathFlags: readonly string[]
17
- scalarFlags: readonly string[]
17
+ queryFlags: readonly string[]
18
18
  }
19
19
 
20
20
  type CustomUploadSummary = {
@@ -23,127 +23,19 @@ type CustomUploadSummary = {
23
23
  data: ReturnType<typeof summarizeUploadResult>
24
24
  }
25
25
 
26
- type AppInventoryItem = {
27
- id: string
28
- name: string
29
- entities: Array<{ id: string; name: string }>
30
- }
31
-
32
26
  function summarizeUploadResult(value: Awaited<ReturnType<typeof uploadLocalFile>>) {
33
27
  return value
34
28
  }
35
29
 
36
- function parseJSONResponse(raw: string): unknown {
37
- if (!raw) return null
38
- try {
39
- return JSON.parse(raw)
40
- } catch {
41
- return raw
42
- }
43
- }
44
-
45
- function unwrapResponseData(payload: unknown): unknown {
46
- return isRecord(payload) && 'data' in payload ? payload.data : payload
47
- }
48
-
49
- function formatUpstreamError(payload: unknown): string {
50
- if (typeof payload === 'string') {
51
- return payload
52
- }
53
- if (isRecord(payload) && isRecord(payload.error) && typeof payload.error.message === 'string') {
54
- return payload.error.message
55
- }
56
- return JSON.stringify(payload)
57
- }
58
-
59
- function extractApps(payload: unknown): Array<{ id: string; name: string }> {
60
- const data = unwrapResponseData(payload)
61
- if (!Array.isArray(data)) return []
62
- return data
63
- .filter((item): item is Record<string, unknown> => isRecord(item))
64
- .filter((item) => item.id !== undefined && typeof item.name === 'string')
65
- .map((item) => ({
66
- id: String(item.id),
67
- name: item.name as string,
68
- }))
69
- }
70
-
71
- function extractEntities(payload: unknown): Array<{ id: string; name: string }> {
72
- const data = unwrapResponseData(payload)
73
- if (!Array.isArray(data)) return []
74
- return data
75
- .filter((item): item is Record<string, unknown> => isRecord(item))
76
- .filter((item) => item.id !== undefined && typeof item.name === 'string')
77
- .map((item) => ({
78
- id: String(item.id),
79
- name: item.name as string,
80
- }))
81
- }
82
-
83
- async function executeAppInventory(runtimeEnv: RuntimeEnv, fetchImpl: typeof fetch) {
84
- const appsResponse = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, '/apps'), {
85
- method: 'GET',
86
- headers: buildRequestHeaders(runtimeEnv),
87
- })
88
- const appsRaw = await appsResponse.text()
89
- const appsPayload = parseJSONResponse(appsRaw)
90
- if (!appsResponse.ok) {
91
- throw new CLIError('UPSTREAM_REQUEST_FAILED', formatUpstreamError(appsPayload), 1)
92
- }
93
- if (isRecord(appsPayload) && appsPayload.error) {
94
- throw new CLIError('UPSTREAM_BODY_ERROR', formatUpstreamError(appsPayload), 1, {
95
- endpoint: '/apps',
96
- method: 'GET',
97
- upstreamError: appsPayload.error,
98
- })
99
- }
100
- const apps = extractApps(appsPayload)
101
- const items: AppInventoryItem[] = []
102
- for (const app of apps) {
103
- const entitiesResponse = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/apps/${encodeURIComponent(app.id)}/entities`), {
104
- method: 'POST',
105
- headers: {
106
- ...buildRequestHeaders(runtimeEnv),
107
- 'Content-Type': 'application/json',
108
- },
109
- body: JSON.stringify({}),
110
- })
111
- const entitiesRaw = await entitiesResponse.text()
112
- const entitiesPayload = parseJSONResponse(entitiesRaw)
113
- if (!entitiesResponse.ok) {
114
- throw new CLIError('UPSTREAM_REQUEST_FAILED', formatUpstreamError(entitiesPayload), 1)
115
- }
116
- if (isRecord(entitiesPayload) && entitiesPayload.error) {
117
- throw new CLIError('UPSTREAM_BODY_ERROR', formatUpstreamError(entitiesPayload), 1, {
118
- endpoint: `/apps/${app.id}/entities`,
119
- method: 'POST',
120
- upstreamError: entitiesPayload.error,
121
- })
122
- }
123
- items.push({
124
- ...app,
125
- entities: extractEntities(entitiesPayload),
126
- })
127
- }
128
- return {
129
- kind: 'result',
130
- commandPath: ['app', 'inventory'] as const,
131
- status: 200,
132
- data: {
133
- apps: items,
134
- },
135
- }
136
- }
137
-
138
30
  export function renderRootHelp(scope: CommandScope): string {
139
31
  const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase'
140
32
  const items = listModules(scope).map((item) => ` - ${item}`)
141
33
  return [
142
- `${binary} <module> <command> [flags]`,
34
+ `${binary} <command> [subcommand] [flags]`,
143
35
  '',
144
36
  ...(scope === 'admin'
145
- ? ['use arcubase-admin for app, entity shell, and schema management', '']
146
- : ['use arcubase for rows, search, selection actions, and bulk actions', '']),
37
+ ? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
38
+ : ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
147
39
  'modules:',
148
40
  ...items,
149
41
  ...(scope === 'user' ? ['','special commands:',' - upload <local-file>'] : []),
@@ -153,62 +45,69 @@ export function renderRootHelp(scope: CommandScope): string {
153
45
  export function renderModuleHelp(scope: CommandScope, moduleName: string): string {
154
46
  const items = listModuleCommands(scope, moduleName)
155
47
  if (items.length === 0) {
156
- const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
48
+ const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`)
157
49
  throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2, suggestions.length > 0 ? { suggestions } : undefined)
158
50
  }
51
+ if (items.length === 1 && items[0].commandPath.length === 1) {
52
+ return renderCommandHelp(scope, moduleName)
53
+ }
159
54
  return [
160
55
  `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
161
56
  '',
162
57
  ...(scope === 'admin'
163
- ? ['use arcubase-admin for app, entity shell, and schema management', '']
164
- : ['use arcubase for rows, search, selection actions, and bulk actions', '']),
58
+ ? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
59
+ : ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
165
60
  'commands:',
166
- ...items.map((item) => ` - ${item.commandPath[1]}`),
61
+ ...items.filter((item) => item.commandPath.length > 1).map((item) => ` - ${item.commandPath[1]}`),
167
62
  ].join('\n')
168
63
  }
169
64
 
170
65
  function buildUnknownCommandDetails(scope: CommandScope, moduleName: string, commandName: string): { suggestions?: string[]; reason?: string; hint?: string } | undefined {
171
- const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`)
172
- if (scope === 'admin' && moduleName === 'workflow' && suggestions.some((item) => item.startsWith('arcubase rows '))) {
173
- return {
174
- suggestions,
175
- reason: `${moduleName} ${commandName} is not a valid command group; use arcubase rows for row operations`,
176
- hint: `use: arcubase rows ${commandName}`,
177
- }
178
- }
66
+ const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`)
179
67
  return suggestions.length > 0 ? { suggestions } : undefined
180
68
  }
181
69
 
182
- export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName: string): string {
70
+ export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName?: string): string {
183
71
  const command = findCommand(scope, moduleName, commandName)
184
72
  if (!command) {
185
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName))
73
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''))
186
74
  }
187
75
  const docHints = [
188
76
  ...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`),
189
77
  ...(command.requestType ? getDocHints(command.requestType) : []),
190
78
  ].filter((item, index, list) => list.findIndex((candidate) => candidate.file === item.file) === index)
79
+ const pathFlags = command.pathParams.filter((item) => !getFixedValue(item)).map((item) => `--${item.flag}`)
80
+ const queryFlags = command.queryParams.filter((item) => !getFixedValue(item)).map((item) => `--${item.flag}`)
191
81
  return [
192
- `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} ${commandName}`,
82
+ `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${command.commandPath.join(' ')}`,
193
83
  `method: ${command.method}`,
194
84
  `endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
195
- command.requestType ? `body: ${command.requestType} via --body-file` : 'body: none',
196
- 'query flags: --query-file',
197
- command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
198
- command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
199
- ...(scope === 'admin' && moduleName === 'entity' && commandName === 'admin-save-entity'
85
+ command.requestType ? `body: ${command.requestType} via --body-file or --body-json` : 'body: none',
86
+ queryFlags.length ? `query flags: ${queryFlags.join(', ')}, --query-file` : 'query flags: --query-file',
87
+ pathFlags.length ? `path flags: ${pathFlags.join(', ')}` : 'path flags: none',
88
+ ...(scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'update-schema'
200
89
  ? [
201
90
  'next step:',
202
91
  ' - switch to arcubase for row operations',
203
- ' - row insert: arcubase rows insert-entity --app-id <app_id> --entity-id <entity_id> --body-file insert.json',
204
- ' - row query: arcubase rows query-entity --app-id <app_id> --entity-id <entity_id> --body-file query.json',
92
+ ' - row create: arcubase row create --app-id <app_id> --table-id <table_id> --body-file insert.json',
93
+ ' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-file query.json',
205
94
  ]
206
95
  : []),
207
- ...(scope === 'admin' && moduleName === 'app' && commandName === 'inventory'
96
+ ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
208
97
  ? [
209
- 'purpose:',
210
- ' - list all accessible apps and each app entity list in one call sequence',
211
- ' - use this before asking the user for app_id',
98
+ 'body-json examples:',
99
+ ' - enable: {"update":["enabled"],"enabled":true}',
100
+ ' - rename: {"update":["name"],"name":"Sales read only"}',
101
+ ' - update options: {"update":["options"],"options":{"user_scope":[{"type":"user","id":123}]}}',
102
+ ]
103
+ : []),
104
+ ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
105
+ ? [
106
+ 'body-json example:',
107
+ ' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
108
+ 'success requires:',
109
+ ' - result.enabled must be true',
110
+ ' - result.options.user_scope must contain the assigned user ids',
212
111
  ]
213
112
  : []),
214
113
  ...(docHints.length > 0
@@ -234,15 +133,22 @@ function coerceScalar(value: string, typeText: string): any {
234
133
  return value
235
134
  }
236
135
 
237
- function resolveEndpoint(endpoint: string, flags: Record<string, string | boolean>): string {
238
- let resolved = endpoint
239
- for (const placeholder of [...endpoint.matchAll(/:([A-Za-z0-9_]+)/g)].map((match) => match[1])) {
240
- const flagName = placeholder.replace(/_/g, '-')
241
- const value = flagValue(flags, flagName)
136
+ function getFixedValue(value: object): string | undefined {
137
+ if (!('fixedValue' in value)) {
138
+ return undefined
139
+ }
140
+ const fixedValue = (value as { fixedValue?: unknown }).fixedValue
141
+ return typeof fixedValue === 'string' ? fixedValue : undefined
142
+ }
143
+
144
+ function resolveEndpoint(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>): string {
145
+ let resolved: string = command.endpoint
146
+ for (const mapping of command.pathParams) {
147
+ const value = getFixedValue(mapping) ?? flagValue(flags, mapping.flag)
242
148
  if (!value) {
243
- throw new CLIError('MISSING_PATH_FLAG', `missing required flag --${flagName}`, 2)
149
+ throw new CLIError('MISSING_PATH_FLAG', `missing required flag --${mapping.flag}`, 2)
244
150
  }
245
- resolved = resolved.replace(`:${placeholder}`, encodeURIComponent(value))
151
+ resolved = resolved.replace(`:${mapping.param}`, encodeURIComponent(value))
246
152
  }
247
153
  return resolved
248
154
  }
@@ -252,11 +158,10 @@ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record
252
158
  return undefined
253
159
  }
254
160
  const out: Record<string, any> = {}
255
- for (const scalar of command.scalarParams) {
256
- const flagName = scalar.name.replace(/([A-Z])/g, '-$1').toLowerCase()
257
- const value = flagValue(flags, flagName)
161
+ for (const scalar of command.queryParams) {
162
+ const value = getFixedValue(scalar) ?? flagValue(flags, scalar.flag)
258
163
  if (value === undefined) continue
259
- out[scalar.name] = coerceScalar(value, scalar.type)
164
+ out[scalar.key] = coerceScalar(value, scalar.type)
260
165
  }
261
166
  return Object.keys(out).length > 0 ? out : undefined
262
167
  }
@@ -292,7 +197,7 @@ async function fetchEntityFields(
292
197
  entityId: string,
293
198
  fetchImpl: typeof fetch,
294
199
  ): Promise<Map<number, EntityFieldInfo>> {
295
- const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
200
+ const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/api/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
296
201
  method: 'GET',
297
202
  headers: buildRequestHeaders(runtimeEnv),
298
203
  })
@@ -315,7 +220,7 @@ async function fetchEntityFields(
315
220
  ? upstreamError
316
221
  : JSON.stringify(upstreamError)
317
222
  throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
318
- endpoint: `/entity/${appId}/${entityId}`,
223
+ endpoint: `/api/entity/${appId}/${entityId}`,
319
224
  method: 'GET',
320
225
  upstreamError,
321
226
  })
@@ -359,7 +264,7 @@ async function validateFileFieldPayloads(
359
264
  return
360
265
  }
361
266
  const appId = flagValue(flags, 'app-id')
362
- const entityId = flagValue(flags, 'entity-id')
267
+ const entityId = flagValue(flags, 'table-id')
363
268
  if (!appId || !entityId) {
364
269
  return
365
270
  }
@@ -445,6 +350,51 @@ function throwBodyValidationFailure(message: string, requestType: string, path:
445
350
  })
446
351
  }
447
352
 
353
+ function stringArray(value: unknown): string[] {
354
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
355
+ }
356
+
357
+ function requireUpdateContains(
358
+ scope: CommandScope,
359
+ command: NonNullable<ReturnType<typeof findCommand>>,
360
+ body: Record<string, unknown>,
361
+ field: string,
362
+ ) {
363
+ if (!(field in body)) return
364
+ const update = stringArray(body.update)
365
+ if (!update.includes(field)) {
366
+ throwBodyValidationFailure(
367
+ `body.update must include "${field}" when body.${field} is present. Example: {"update":["${field}"],"${field}":${field === 'enabled' ? 'true' : '"value"'}}`,
368
+ command.requestType ?? 'AppIngressUpdateReqVO',
369
+ 'body.update',
370
+ scope,
371
+ command,
372
+ )
373
+ }
374
+ }
375
+
376
+ function validateActionSpecificRawBody(
377
+ scope: CommandScope,
378
+ command: NonNullable<ReturnType<typeof findCommand>>,
379
+ body: unknown,
380
+ ) {
381
+ if (!isRecord(body) || !command.requestType) {
382
+ return
383
+ }
384
+
385
+ if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users') {
386
+ if ('users' in body || 'allowedUsers' in body) {
387
+ throwBodyValidationFailure(
388
+ 'access-rule assign-users does not accept users or allowedUsers; use body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
389
+ command.requestType,
390
+ 'body.options.user_scope',
391
+ scope,
392
+ command,
393
+ )
394
+ }
395
+ }
396
+ }
397
+
448
398
  function validateActionSpecificBody(
449
399
  scope: CommandScope,
450
400
  command: NonNullable<ReturnType<typeof findCommand>>,
@@ -455,7 +405,46 @@ function validateActionSpecificBody(
455
405
  return
456
406
  }
457
407
 
458
- if (command.module === 'rows' && command.functionName === 'queryEntitySelection') {
408
+ if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update') {
409
+ requireUpdateContains(scope, command, body, 'enabled')
410
+ requireUpdateContains(scope, command, body, 'name')
411
+ requireUpdateContains(scope, command, body, 'options')
412
+ }
413
+
414
+ if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users') {
415
+ const update = stringArray(body.update)
416
+ const options = isRecord(body.options) ? body.options : undefined
417
+ const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined
418
+ if ('users' in body || 'allowedUsers' in body) {
419
+ throwBodyValidationFailure(
420
+ 'access-rule assign-users does not accept users or allowedUsers; use body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":123}]}}',
421
+ command.requestType,
422
+ 'body.options.user_scope',
423
+ scope,
424
+ command,
425
+ )
426
+ }
427
+ if (!update.includes('user_scope')) {
428
+ throwBodyValidationFailure(
429
+ 'access-rule assign-users requires body.update to include "user_scope"',
430
+ command.requestType,
431
+ 'body.update',
432
+ scope,
433
+ command,
434
+ )
435
+ }
436
+ if (!userScope || userScope.length === 0) {
437
+ throwBodyValidationFailure(
438
+ 'access-rule assign-users requires non-empty body.options.user_scope',
439
+ command.requestType,
440
+ 'body.options.user_scope',
441
+ scope,
442
+ command,
443
+ )
444
+ }
445
+ }
446
+
447
+ if (command.commandPath[0] === 'row' && command.commandPath[1] === 'selection-action') {
459
448
  const action = flagValue(flags, 'action')
460
449
  const selection = body.selection
461
450
  if (!isRecord(selection) || typeof selection.type !== 'string') {
@@ -469,7 +458,7 @@ function validateActionSpecificBody(
469
458
  if (action === 'query') {
470
459
  if (type === 'condition') {
471
460
  throwBodyValidationFailure(
472
- 'selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use rows query-entity for search conditions',
461
+ 'selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use row query for search conditions',
473
462
  command.requestType,
474
463
  'body.selection.type',
475
464
  scope,
@@ -510,10 +499,10 @@ export function summarizeCommand(scope: CommandScope, moduleName: string, comman
510
499
  scope,
511
500
  commandPath: command.commandPath,
512
501
  method: command.method,
513
- endpoint: normalizeExternalEndpoint(command.endpoint),
502
+ endpoint: command.endpoint,
514
503
  requestType: command.requestType,
515
- pathFlags: command.endpointParams,
516
- scalarFlags: command.scalarParams.map((item) => item.name.replace(/([A-Z])/g, '-$1').toLowerCase()),
504
+ pathFlags: command.pathParams.filter((item) => !getFixedValue(item)).map((item) => item.flag),
505
+ queryFlags: command.queryParams.filter((item) => !getFixedValue(item)).map((item) => item.flag),
517
506
  }
518
507
  }
519
508
 
@@ -551,7 +540,8 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
551
540
  if (!moduleName) {
552
541
  return { kind: 'help', text: renderRootHelp(scope) }
553
542
  }
554
- if (!commandName) {
543
+ const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null
544
+ if (!commandName && !singleTokenCommand) {
555
545
  return { kind: 'help', text: renderModuleHelp(scope, moduleName) }
556
546
  }
557
547
  if (hasFlag(parsed.flags, 'help')) {
@@ -562,23 +552,25 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
562
552
 
563
553
  const command = findCommand(scope, moduleName, commandName)
564
554
  if (!command) {
565
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName))
566
- }
567
- if (scope === 'admin' && moduleName === 'app' && commandName === 'inventory') {
568
- return executeAppInventory(runtimeEnv, fetchImpl)
555
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''))
569
556
  }
570
557
 
571
- const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags))
558
+ const endpoint = resolveEndpoint(command, parsed.flags)
572
559
  const scalarQuery = buildScalarQuery(command, parsed.flags)
573
560
  const fileQuery = loadQueryFromFlags(parsed.flags)
574
561
  const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) }
575
562
  const body = loadBodyFromFlags(parsed.flags)
576
563
  let validatedBody = body
577
564
 
565
+ if (!command.requestType && body !== undefined) {
566
+ throw new CLIError('BODY_JSON_NOT_SUPPORTED', `command does not accept a request body: ${moduleName} ${commandName}`, 2)
567
+ }
568
+
578
569
  if (command.requestType) {
579
570
  if (body === undefined) {
580
- throw new CLIError('MISSING_BODY_FILE', `command requires --body-file for ${command.requestType}`, 2)
571
+ throw new CLIError('MISSING_BODY_FILE', `command requires --body-file or --body-json for ${command.requestType}`, 2)
581
572
  }
573
+ validateActionSpecificRawBody(scope, command, body)
582
574
  const schema = getBodySchema(command.requestType)
583
575
  if (schema) {
584
576
  const parsedBody = schema.safeParse(body)
@@ -22,7 +22,7 @@ export function normalizeExternalEndpoint(endpoint: string): string {
22
22
  }
23
23
 
24
24
  export function buildURL(baseURL: string, endpoint: string, query?: Record<string, any>): string {
25
- const url = new URL(normalizeExternalEndpoint(endpoint), baseURL.endsWith('/') ? baseURL : `${baseURL}/`)
25
+ const url = new URL(endpoint, baseURL.endsWith('/') ? baseURL : `${baseURL}/`)
26
26
  if (query) {
27
27
  for (const [key, value] of Object.entries(query)) {
28
28
  if (value === undefined || value === null) continue
@@ -64,7 +64,7 @@ function requireTokenPayload(payload: UploadTokenResponse): NonNullable<UploadTo
64
64
  ? upstreamError
65
65
  : JSON.stringify(upstreamError)
66
66
  throw new CLIError('UPSTREAM_BODY_ERROR', message, 1, {
67
- endpoint: '/upload/token',
67
+ endpoint: '/api/upload/token',
68
68
  method: 'POST',
69
69
  traceId: typeof payload.trace_id === 'string' ? payload.trace_id : undefined,
70
70
  upstreamError,
@@ -116,7 +116,7 @@ export async function uploadLocalFile(
116
116
  const { absolutePath, bytes } = readUploadSource(sourcePath)
117
117
  const filename = (options.filename && options.filename.trim()) || path.basename(absolutePath)
118
118
 
119
- const tokenResponse = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, '/upload/token'), {
119
+ const tokenResponse = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, '/api/upload/token'), {
120
120
  method: 'POST',
121
121
  headers: {
122
122
  ...buildRequestHeaders(env),
@@ -131,7 +131,7 @@ export async function uploadLocalFile(
131
131
  const tokenParsed = parseJSONResponse(tokenRaw)
132
132
  if (!tokenResponse.ok) {
133
133
  throw new CLIError('UPLOAD_TOKEN_REQUEST_FAILED', typeof tokenParsed === 'string' ? tokenParsed : JSON.stringify(tokenParsed), 1, {
134
- endpoint: '/upload/token',
134
+ endpoint: '/api/upload/token',
135
135
  method: 'POST',
136
136
  })
137
137
  }