@carthooks/arcubase-cli 0.1.10 → 0.1.13

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 (105) hide show
  1. package/bundle/arcubase-admin.mjs +1365 -375
  2. package/bundle/arcubase.mjs +1365 -375
  3. package/dist/generated/command_registry.generated.d.ts +36 -35
  4. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  5. package/dist/generated/command_registry.generated.js +37 -36
  6. package/dist/generated/type_index.generated.d.ts +23 -23
  7. package/dist/generated/type_index.generated.js +23 -23
  8. package/dist/runtime/entity_save_schema.d.ts.map +1 -1
  9. package/dist/runtime/entity_save_schema.js +30 -0
  10. package/dist/runtime/env.d.ts +2 -1
  11. package/dist/runtime/env.d.ts.map +1 -1
  12. package/dist/runtime/env.js +10 -1
  13. package/dist/runtime/errors.d.ts +3 -0
  14. package/dist/runtime/errors.d.ts.map +1 -1
  15. package/dist/runtime/execute.d.ts +4 -2
  16. package/dist/runtime/execute.d.ts.map +1 -1
  17. package/dist/runtime/execute.js +1046 -134
  18. package/dist/runtime/paths.d.ts +1 -1
  19. package/dist/runtime/paths.d.ts.map +1 -1
  20. package/dist/runtime/paths.js +9 -2
  21. package/dist/runtime/upload.d.ts +1 -11
  22. package/dist/runtime/upload.d.ts.map +1 -1
  23. package/dist/runtime/upload.js +19 -133
  24. package/dist/runtime/zod_registry.d.ts +5 -3
  25. package/dist/runtime/zod_registry.d.ts.map +1 -1
  26. package/dist/runtime/zod_registry.js +108 -48
  27. package/dist/tests/bootstrap.test.js +12 -0
  28. package/dist/tests/command_registry.test.js +8 -0
  29. package/dist/tests/docs_readability.test.d.ts +2 -0
  30. package/dist/tests/docs_readability.test.d.ts.map +1 -0
  31. package/dist/tests/docs_readability.test.js +87 -0
  32. package/dist/tests/entity_save_schema.test.js +22 -0
  33. package/dist/tests/execute_validation.test.js +443 -44
  34. package/dist/tests/help.test.js +47 -8
  35. package/dist/tests/upload.test.js +30 -77
  36. package/package.json +1 -1
  37. package/sdk-dist/docs/runtime-reference/README.md +43 -29
  38. package/sdk-dist/docs/runtime-reference/access-rule.md +128 -0
  39. package/sdk-dist/docs/runtime-reference/condition.md +182 -0
  40. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +6 -5
  41. package/sdk-dist/docs/runtime-reference/entity-schema/checkbox.md +1 -1
  42. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +2 -2
  43. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +2 -2
  44. package/sdk-dist/docs/runtime-reference/entity-schema/linkto.md +3 -3
  45. package/sdk-dist/docs/runtime-reference/entity-schema/number.md +1 -1
  46. package/sdk-dist/docs/runtime-reference/entity-schema/query.md +3 -3
  47. package/sdk-dist/docs/runtime-reference/entity-schema/relation.md +3 -3
  48. package/sdk-dist/docs/runtime-reference/entity-schema/relationfield.md +3 -3
  49. package/sdk-dist/docs/runtime-reference/entity-schema/status.md +1 -1
  50. package/sdk-dist/docs/runtime-reference/entity-schema/text.md +1 -1
  51. package/sdk-dist/docs/runtime-reference/entity-schema.md +28 -21
  52. package/sdk-dist/docs/runtime-reference/examples/README.md +1 -1
  53. package/sdk-dist/docs/runtime-reference/examples/crm-01/README.md +30 -11
  54. package/sdk-dist/docs/runtime-reference/examples/crm-01/contact.create-entity.json +1 -0
  55. package/sdk-dist/docs/runtime-reference/examples/crm-01/contact.schema.json +1 -0
  56. package/sdk-dist/docs/runtime-reference/examples/crm-01/create-app.json +1 -0
  57. package/sdk-dist/docs/runtime-reference/examples/crm-01/customer.create-entity.json +1 -0
  58. package/sdk-dist/docs/runtime-reference/examples/crm-01/customer.schema.json +1 -0
  59. package/sdk-dist/docs/runtime-reference/examples/crm-01/follow-up.create-entity.json +1 -0
  60. package/sdk-dist/docs/runtime-reference/examples/crm-01/follow-up.schema.json +1 -0
  61. package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.bulk-update.json +1 -0
  62. package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.create-entity.json +1 -0
  63. package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.query.json +1 -0
  64. package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.row.insert.json +1 -0
  65. package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.schema.json +1 -0
  66. package/sdk-dist/docs/runtime-reference/examples/crm-01/lead.selection.condition.json +1 -0
  67. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +26 -26
  68. package/sdk-dist/docs/runtime-reference/examples/oms-01/sales-order.row.insert.json +1 -1
  69. package/sdk-dist/docs/runtime-reference/examples/wms-01/README.md +26 -10
  70. package/sdk-dist/docs/runtime-reference/examples/wms-01/create-app.json +1 -0
  71. package/sdk-dist/docs/runtime-reference/examples/wms-01/goods-receipt.create-entity.json +1 -0
  72. package/sdk-dist/docs/runtime-reference/examples/wms-01/goods-receipt.schema.json +1 -0
  73. package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.bulk-update.json +1 -0
  74. package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.create-entity.json +1 -0
  75. package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.query.json +1 -0
  76. package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.schema.json +1 -0
  77. package/sdk-dist/docs/runtime-reference/examples/wms-01/inventory-snapshot.selection.condition.json +1 -0
  78. package/sdk-dist/docs/runtime-reference/examples/wms-01/item-master.create-entity.json +1 -0
  79. package/sdk-dist/docs/runtime-reference/examples/wms-01/item-master.schema.json +1 -0
  80. package/sdk-dist/docs/runtime-reference/examples/wms-01/stock-issue.create-entity.json +1 -0
  81. package/sdk-dist/docs/runtime-reference/examples/wms-01/stock-issue.schema.json +1 -0
  82. package/sdk-dist/docs/runtime-reference/row-crud.md +48 -8
  83. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +31 -95
  84. package/sdk-dist/docs/runtime-reference/selection.md +128 -0
  85. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +113 -30
  86. package/sdk-dist/docs/runtime-reference/uploads.md +30 -59
  87. package/sdk-dist/docs/runtime-reference/workflow/README.md +129 -13
  88. package/sdk-dist/generated/command_registry.generated.ts +37 -36
  89. package/sdk-dist/generated/type_index.generated.ts +23 -23
  90. package/src/generated/command_registry.generated.ts +37 -36
  91. package/src/generated/type_index.generated.ts +23 -23
  92. package/src/runtime/entity_save_schema.ts +30 -0
  93. package/src/runtime/env.ts +14 -2
  94. package/src/runtime/errors.ts +3 -0
  95. package/src/runtime/execute.ts +1272 -134
  96. package/src/runtime/paths.ts +11 -2
  97. package/src/runtime/upload.ts +19 -178
  98. package/src/runtime/zod_registry.ts +113 -48
  99. package/src/tests/bootstrap.test.ts +16 -0
  100. package/src/tests/command_registry.test.ts +9 -0
  101. package/src/tests/docs_readability.test.ts +96 -0
  102. package/src/tests/entity_save_schema.test.ts +22 -0
  103. package/src/tests/execute_validation.test.ts +577 -47
  104. package/src/tests/help.test.ts +51 -8
  105. package/src/tests/upload.test.ts +34 -91
@@ -5,7 +5,7 @@ import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http
5
5
  import { listModules, listModuleCommands, findCommand, findCommandSuggestions, type CommandScope } from './command_registry.js'
6
6
  import { loadBodyFromFlags, loadQueryFromFlags } from './body_loader.js'
7
7
  import { getBodySchema, getCommandDocHints, getDocHints, getTypeHints, getUnsupportedBodySchemaReason } from './zod_registry.js'
8
- import { renderUploadHelp, uploadLocalFile } from './upload.js'
8
+ import { resolveArcubaseSDKPath } from './paths.js'
9
9
 
10
10
  export type CommandExecutionSummary = {
11
11
  scope: CommandScope
@@ -17,16 +17,6 @@ export type CommandExecutionSummary = {
17
17
  queryFlags: readonly string[]
18
18
  }
19
19
 
20
- type CustomUploadSummary = {
21
- kind: 'upload'
22
- status: number
23
- data: ReturnType<typeof summarizeUploadResult>
24
- }
25
-
26
- function summarizeUploadResult(value: Awaited<ReturnType<typeof uploadLocalFile>>) {
27
- return value
28
- }
29
-
30
20
  export function renderRootHelp(scope: CommandScope): string {
31
21
  const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase'
32
22
  const items = listModules(scope).map((item) => ` - ${item}`)
@@ -36,20 +26,21 @@ export function renderRootHelp(scope: CommandScope): string {
36
26
  ...(scope === 'admin'
37
27
  ? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
38
28
  : ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
29
+ 'prefer --body-json <json-string> for normal payloads, including table schema; use --body-file only when JSON is too large for one tool call',
30
+ '',
39
31
  'modules:',
40
32
  ...items,
41
- ...(scope === 'user' ? ['','special commands:',' - upload <local-file>'] : []),
42
33
  ].join('\n')
43
34
  }
44
35
 
45
- export function renderModuleHelp(scope: CommandScope, moduleName: string): string {
36
+ export function renderModuleHelp(scope: CommandScope, moduleName: string, env: EnvInput = process.env): string {
46
37
  const items = listModuleCommands(scope, moduleName)
47
38
  if (items.length === 0) {
48
39
  const suggestions = findCommandSuggestions(moduleName, '').map((item) => `${item.binary} ${item.moduleName}${item.commandName ? ` ${item.commandName}` : ''}`)
49
40
  throw new CLIError('UNKNOWN_MODULE', `unknown module: ${moduleName}`, 2, suggestions.length > 0 ? { suggestions } : undefined)
50
41
  }
51
42
  if (items.length === 1 && items[0].commandPath.length === 1) {
52
- return renderCommandHelp(scope, moduleName)
43
+ return renderCommandHelp(scope, moduleName, undefined, env)
53
44
  }
54
45
  return [
55
46
  `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
@@ -57,6 +48,8 @@ export function renderModuleHelp(scope: CommandScope, moduleName: string): strin
57
48
  ...(scope === 'admin'
58
49
  ? ['use arcubase-admin for app, table, access-rule, and workflow management', '']
59
50
  : ['use arcubase for entry, table, row, upload, profile, and workflow actions', '']),
51
+ 'prefer --body-json <json-string> for normal payloads, including table schema; use --body-file only when JSON is too large for one tool call',
52
+ '',
60
53
  'commands:',
61
54
  ...items.filter((item) => item.commandPath.length > 1).map((item) => ` - ${item.commandPath[1]}`),
62
55
  ].join('\n')
@@ -67,30 +60,55 @@ function buildUnknownCommandDetails(scope: CommandScope, moduleName: string, com
67
60
  return suggestions.length > 0 ? { suggestions } : undefined
68
61
  }
69
62
 
70
- export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName?: string): string {
63
+ type EnvInput = Record<string, string | undefined>
64
+
65
+ function sdkPath(file: string, env: EnvInput = process.env): string {
66
+ return resolveArcubaseSDKPath(file, env)
67
+ }
68
+
69
+ export function renderCommandHelp(scope: CommandScope, moduleName: string, commandName?: string, env: EnvInput = process.env): string {
71
70
  const command = findCommand(scope, moduleName, commandName)
72
71
  if (!command) {
73
72
  throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''))
74
73
  }
75
74
  const docHints = [
76
- ...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`),
77
- ...(command.requestType ? getDocHints(command.requestType) : []),
75
+ ...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`, env),
76
+ ...(command.requestType ? getDocHints(command.requestType, env) : []),
78
77
  ].filter((item, index, list) => list.findIndex((candidate) => candidate.file === item.file) === index)
78
+ const typeHints = command.requestType ? getTypeHints(command.requestType, env) : []
79
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}`)
80
+ const queryFlags = visibleQueryParams(command).map((item) => `--${item.flag}`)
81
+ const isTableCreate = scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'create'
82
+ const bodyTypeText = isTableCreate ? 'TableCreateWithSchema' : command.requestType
83
+ const tableCreateExampleBody = JSON.stringify(buildTableCreateExampleBody())
81
84
  return [
82
85
  `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${command.commandPath.join(' ')}`,
83
86
  `method: ${command.method}`,
84
87
  `endpoint: ${normalizeExternalEndpoint(command.endpoint)}`,
85
- command.requestType ? `body: ${command.requestType} via --body-file or --body-json` : 'body: none',
88
+ command.requestType ? `body: ${bodyTypeText} via --body-json <json-string> | --body-file <file>.json` : 'body: none',
86
89
  queryFlags.length ? `query flags: ${queryFlags.join(', ')}, --query-file` : 'query flags: --query-file',
87
90
  pathFlags.length ? `path flags: ${pathFlags.join(', ')}` : 'path flags: none',
91
+ ...(isTableCreate
92
+ ? [
93
+ 'body-json example:',
94
+ ` - ${tableCreateExampleBody}`,
95
+ 'body rules:',
96
+ ' - fields must be non-empty; empty table shell creation is not allowed',
97
+ ' - field_id_seq must be greater than every field.id',
98
+ ' - use text, not string; use member, not user',
99
+ ' - for select/radio/checkbox/status, use options.options.items',
100
+ 'schema refs:',
101
+ ` - Table schema cheatsheet: ${sdkPath('docs/runtime-reference/entity-schema.md', env)}`,
102
+ ` - Field type index: ${sdkPath('docs/runtime-reference/entity-schema/README.md', env)}`,
103
+ ]
104
+ : []),
88
105
  ...(scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'update-schema'
89
106
  ? [
90
107
  'next step:',
108
+ ' - prefer --body-json for schema save to avoid file creation and JSON file corruption',
91
109
  ' - switch to arcubase for row operations',
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',
110
+ ' - row create: arcubase row create --app-id <app_id> --table-id <table_id> --body-json <json-string> | --body-file insert.json',
111
+ ' - row query: arcubase row query --app-id <app_id> --table-id <table_id> --body-json <json-string> | --body-file query.json',
94
112
  ]
95
113
  : []),
96
114
  ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
@@ -98,13 +116,24 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
98
116
  'body-json examples:',
99
117
  ' - enable: {"update":["enabled"],"enabled":true}',
100
118
  ' - rename: {"update":["name"],"name":"Sales read only"}',
101
- ' - update options: {"update":["options"],"options":{"user_scope":[{"type":"member","id":123}]}}',
119
+ ' - update policy: {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
120
+ ]
121
+ : []),
122
+ ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'create'
123
+ ? [
124
+ 'body-json examples:',
125
+ ` - full access: ${JSON.stringify(buildFullAccessRuleBody())}`,
126
+ ` - hide field: ${JSON.stringify(buildHiddenFieldAccessRuleBody())}`,
127
+ 'success requires:',
128
+ ' - result.enabled must be true',
129
+ ' - options.user_scope[].type is "user" for tenant users',
130
+ ' - hidden fields use colon field keys such as ":1002"',
102
131
  ]
103
132
  : []),
104
133
  ...(scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
105
134
  ? [
106
135
  'body-json example:',
107
- ' - {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
136
+ ' - {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
108
137
  'success requires:',
109
138
  ' - result.enabled must be true',
110
139
  ' - result.options.user_scope must contain the assigned numeric TenantUser.ID values',
@@ -116,6 +145,9 @@ export function renderCommandHelp(scope: CommandScope, moduleName: string, comma
116
145
  ...(docHints.length > 0
117
146
  ? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
118
147
  : []),
148
+ ...(typeHints.length > 0
149
+ ? ['types:', ...typeHints.map((item) => ` - ${item.symbol}: ${item.file}`)]
150
+ : []),
119
151
  ].join('\n')
120
152
  }
121
153
 
@@ -161,7 +193,7 @@ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record
161
193
  return undefined
162
194
  }
163
195
  const out: Record<string, any> = {}
164
- for (const scalar of command.queryParams) {
196
+ for (const scalar of effectiveQueryParams(command)) {
165
197
  const value = getFixedValue(scalar) ?? flagValue(flags, scalar.flag)
166
198
  if (value === undefined) continue
167
199
  out[scalar.key] = coerceScalar(value, scalar.type)
@@ -169,10 +201,133 @@ function buildScalarQuery(command: ReturnType<typeof findCommand>, flags: Record
169
201
  return Object.keys(out).length > 0 ? out : undefined
170
202
  }
171
203
 
204
+ function validateKnownFlags(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>, env: EnvInput = process.env) {
205
+ const allowed = new Set<string>(['help', 'body-file', 'body-json', 'query-file'])
206
+ for (const mapping of command.pathParams) {
207
+ if (!getFixedValue(mapping)) {
208
+ allowed.add(mapping.flag)
209
+ }
210
+ }
211
+ for (const query of visibleQueryParams(command)) {
212
+ if (!getFixedValue(query)) {
213
+ allowed.add(query.flag)
214
+ }
215
+ }
216
+ for (const flag of Object.keys(flags)) {
217
+ if (!allowed.has(flag)) {
218
+ throw new CLIError('UNKNOWN_FLAG', `unknown flag --${flag} for ${command.commandPath.join(' ')}`, 2, {
219
+ ...buildUnknownFlagDetails(command, flag, env),
220
+ })
221
+ }
222
+ }
223
+ }
224
+
225
+ function buildUnknownFlagDetails(
226
+ command: NonNullable<ReturnType<typeof findCommand>>,
227
+ flag: string,
228
+ env: EnvInput = process.env,
229
+ ): CLIErrorDetails {
230
+ const operation = command.commandPath.join(' ')
231
+ const details: CLIErrorDetails = {
232
+ operation,
233
+ hint: `run ${operation} --help for the supported flags`,
234
+ }
235
+ if (command.requestType) {
236
+ details.requestType = command.requestType
237
+ details.tsHints = getTypeHints(command.requestType, env)
238
+ details.docHints = getDocHints(command.requestType, env)
239
+ details.commonMistakes = [
240
+ `do not pass request body fields as --${flag}`,
241
+ 'put request body fields inside --body-json <json-string>',
242
+ ]
243
+ details.suggestions = [`retry with: ${command.commandPath.join(' ')} ${command.pathParams.filter((item) => !getFixedValue(item)).map((item) => `--${item.flag} <${item.flag.replace(/-/g, '_')}>`).join(' ')} --body-json '<json>'`.replace(/\s+/g, ' ').trim()]
244
+ }
245
+ if (command.commandPath[0] === 'row' && command.commandPath[1] === 'query' && ['limit', 'offset', 'search', 'sorts', 'view-mode'].includes(flag)) {
246
+ details.shapeHint = { limit: 20, offset: 0, search: { text_search: 'SO-1001' } }
247
+ details.suggestions = ['retry with: row query --app-id <app_id> --table-id <table_id> --body-json \'{"limit":20,"offset":0}\'']
248
+ }
249
+ if (command.commandPath[0] === 'table' && command.commandPath[1] === 'dataset' && ['app-id', 'table-id'].includes(flag)) {
250
+ details.hint = 'table dataset uses --dataset-id and --node-id; use table get for app/table metadata'
251
+ details.suggestions = [
252
+ 'retry metadata read with: table get --app-id <app_id> --table-id <table_id>',
253
+ 'retry dataset read with: table dataset --dataset-id <dataset_id> --node-id <node_id>',
254
+ ]
255
+ }
256
+ return details
257
+ }
258
+
259
+ function visibleQueryParams(command: ReturnType<typeof findCommand>) {
260
+ if (!command) return []
261
+ return effectiveQueryParams(command).filter((item) => !getFixedValue(item))
262
+ }
263
+
264
+ function effectiveQueryParams(command: ReturnType<typeof findCommand>) {
265
+ if (!command) return []
266
+ return command.queryParams.filter((item) => {
267
+ if (rowCommandNeedsPolicy(command) && item.key === 'policy_id') {
268
+ return false
269
+ }
270
+ return true
271
+ })
272
+ }
273
+
172
274
  function isRecord(value: unknown): value is Record<string, any> {
173
275
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
174
276
  }
175
277
 
278
+ function stringifyUpstreamBody(value: unknown): string {
279
+ if (typeof value === 'string') return value
280
+ if (value == null) return ''
281
+ try {
282
+ return JSON.stringify(value)
283
+ } catch {
284
+ return String(value)
285
+ }
286
+ }
287
+
288
+ function extractTraceID(value: unknown): string | undefined {
289
+ if (!isRecord(value)) return undefined
290
+ if (typeof value.trace_id === 'string') return value.trace_id
291
+ if (typeof value.request_id === 'string') return value.request_id
292
+ return undefined
293
+ }
294
+
295
+ function upstreamErrorMessage(value: unknown): string {
296
+ if (isRecord(value) && typeof value.message === 'string') return value.message
297
+ if (typeof value === 'string') return value
298
+ return stringifyUpstreamBody(value)
299
+ }
300
+
301
+ function throwUpstreamHTTPError(endpoint: string, method: string, response: Response, parsedResponse: unknown): never {
302
+ const upstreamError = isRecord(parsedResponse) && parsedResponse.error ? parsedResponse.error : undefined
303
+ const body = stringifyUpstreamBody(parsedResponse)
304
+ throw new CLIError(
305
+ 'UPSTREAM_REQUEST_FAILED',
306
+ upstreamError ? upstreamErrorMessage(upstreamError) : body || `HTTP ${response.status}`,
307
+ 1,
308
+ {
309
+ endpoint,
310
+ method,
311
+ status: response.status,
312
+ traceId: extractTraceID(parsedResponse),
313
+ responseBody: body,
314
+ ...(upstreamError ? { upstreamError } : {}),
315
+ },
316
+ )
317
+ }
318
+
319
+ async function fetchUpstream(url: string, init: RequestInit, endpoint: string, method: string, fetchImpl: typeof fetch): Promise<Response> {
320
+ try {
321
+ return await fetchImpl(url, init)
322
+ } catch (error) {
323
+ throw new CLIError('UPSTREAM_NETWORK_ERROR', error instanceof Error ? error.message : String(error), 1, {
324
+ endpoint,
325
+ method,
326
+ reason: 'network or DNS failure before Arcubase returned an HTTP response',
327
+ })
328
+ }
329
+ }
330
+
176
331
  type EntityFieldInfo = {
177
332
  id: number
178
333
  type: string
@@ -200,10 +355,12 @@ async function fetchEntityFields(
200
355
  entityId: string,
201
356
  fetchImpl: typeof fetch,
202
357
  ): Promise<Map<number, EntityFieldInfo>> {
203
- const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/api/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
358
+ const endpoint = `/api/entity/${appId}/${entityId}`
359
+ const encodedEndpoint = `/api/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`
360
+ const response = await fetchUpstream(buildURL(runtimeEnv.ARCUBASE_BASE_URL, encodedEndpoint), {
204
361
  method: 'GET',
205
362
  headers: buildRequestHeaders(runtimeEnv),
206
- })
363
+ }, endpoint, 'GET', fetchImpl)
207
364
  const raw = await response.text()
208
365
  let parsedResponse: unknown = raw
209
366
  try {
@@ -212,19 +369,15 @@ async function fetchEntityFields(
212
369
  parsedResponse = raw
213
370
  }
214
371
  if (!response.ok) {
215
- throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1)
372
+ throwUpstreamHTTPError(endpoint, 'GET', response, parsedResponse)
216
373
  }
217
374
  if (isRecord(parsedResponse) && parsedResponse.error) {
218
375
  const upstreamError = parsedResponse.error
219
- const upstreamMessage =
220
- isRecord(upstreamError) && typeof upstreamError.message === 'string'
221
- ? upstreamError.message
222
- : typeof upstreamError === 'string'
223
- ? upstreamError
224
- : JSON.stringify(upstreamError)
376
+ const upstreamMessage = upstreamErrorMessage(upstreamError)
225
377
  throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
226
- endpoint: `/api/entity/${appId}/${entityId}`,
378
+ endpoint,
227
379
  method: 'GET',
380
+ traceId: extractTraceID(parsedResponse),
228
381
  upstreamError,
229
382
  })
230
383
  }
@@ -285,52 +438,52 @@ async function validateFileFieldPayloads(
285
438
  const fieldName = field.label ? `${field.label} (${field.type})` : `${field.type} field ${fieldId}`
286
439
  if (typeof value === 'string') {
287
440
  const guidance = looksLikeLocalFilePath(value)
288
- ? `do not put a local file path directly into ${fieldName}; run "arcubase upload ${value}" first and use the returned data array as the field value`
289
- : `${fieldName} must be an array returned by "arcubase upload <local-file>", not a string`
441
+ ? `do not put a local file path directly into ${fieldName}; use arcubase upload token flow and pass the returned upload value array`
442
+ : `${fieldName} must be an upload value array, not a string`
290
443
  throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
291
444
  operation: `${scope}.${command.module}.${command.functionName}`,
292
445
  requestType: command.requestType ?? undefined,
293
446
  issues: [{ path: fieldPath, message: guidance }],
294
447
  docHints: [
295
- { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
296
- { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
448
+ { title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
449
+ { title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
297
450
  ],
298
451
  })
299
452
  }
300
453
  if (isRecord(value)) {
301
- const guidance = `${fieldName} must be an array. Run "arcubase upload <local-file>" and use the returned data array directly as the field value`
454
+ const guidance = `${fieldName} must be an upload value array. Use the upload token flow and pass the returned array directly`
302
455
  throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
303
456
  operation: `${scope}.${command.module}.${command.functionName}`,
304
457
  requestType: command.requestType ?? undefined,
305
458
  issues: [{ path: fieldPath, message: guidance }],
306
459
  docHints: [
307
- { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
308
- { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
460
+ { title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
461
+ { title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
309
462
  ],
310
463
  })
311
464
  }
312
465
  if (!Array.isArray(value)) {
313
- const guidance = `${fieldName} must be an array returned by "arcubase upload <local-file>"`
466
+ const guidance = `${fieldName} must be an upload value array`
314
467
  throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
315
468
  operation: `${scope}.${command.module}.${command.functionName}`,
316
469
  requestType: command.requestType ?? undefined,
317
470
  issues: [{ path: fieldPath, message: guidance }],
318
471
  docHints: [
319
- { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
320
- { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
472
+ { title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
473
+ { title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
321
474
  ],
322
475
  })
323
476
  }
324
477
  for (const [index, item] of value.entries()) {
325
478
  if (!isValidUploadEntry(item)) {
326
- const guidance = `${fieldName} entries must include upload_id or assets_id plus file and file_name. Use "arcubase upload <local-file>" and copy the returned array item as-is`
479
+ const guidance = `${fieldName} entries must include upload_id or assets_id plus file and file_name. Use the upload token flow and copy the returned array item as-is`
327
480
  throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
328
481
  operation: `${scope}.${command.module}.${command.functionName}`,
329
482
  requestType: command.requestType ?? undefined,
330
483
  issues: [{ path: `${fieldPath}.${index}`, message: guidance }],
331
484
  docHints: [
332
- { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
333
- { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
485
+ { title: 'Uploads', file: sdkPath('docs/runtime-reference/uploads.md', runtimeEnv) },
486
+ { title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
334
487
  ],
335
488
  })
336
489
  }
@@ -338,31 +491,152 @@ async function validateFileFieldPayloads(
338
491
  }
339
492
  }
340
493
 
494
+ function buildTableCreateExampleBody() {
495
+ return {
496
+ name: '投票项目',
497
+ parent: 0,
498
+ schema_version: 0,
499
+ field_id_seq: 1003,
500
+ layout: [[1001], [1002]],
501
+ fields: [
502
+ {
503
+ id: 1001,
504
+ label: '投票名称',
505
+ type: 'text',
506
+ required: true,
507
+ unique: false,
508
+ editable: true,
509
+ visible: true,
510
+ show_label: true,
511
+ scannable: false,
512
+ default_value_mode: 0,
513
+ description: '',
514
+ value: null,
515
+ number_decimal: 0,
516
+ depends: [],
517
+ key: 'vote_name',
518
+ code_index: false,
519
+ options: { type: 'text', placeholder: '', lengthLimit: false, lengthMin: 0, lengthMax: 255 },
520
+ transfers: null,
521
+ children: null,
522
+ },
523
+ {
524
+ id: 1002,
525
+ label: '投票描述',
526
+ type: 'textarea',
527
+ required: false,
528
+ unique: false,
529
+ editable: true,
530
+ visible: true,
531
+ show_label: true,
532
+ scannable: false,
533
+ default_value_mode: 0,
534
+ description: '',
535
+ value: null,
536
+ number_decimal: 0,
537
+ depends: [],
538
+ key: 'vote_description',
539
+ code_index: false,
540
+ options: { placeholder: '', html: false, size: 'default', lengthLimit: false, lengthMin: 0, lengthMax: 2000 },
541
+ transfers: null,
542
+ children: null,
543
+ },
544
+ ],
545
+ options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
546
+ workflow_enabled: false,
547
+ }
548
+ }
549
+
341
550
  function isAssignUsersCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
342
551
  return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'assign-users'
343
552
  }
344
553
 
554
+ function isTableCreateCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
555
+ return scope === 'admin' && command.commandPath[0] === 'table' && command.commandPath[1] === 'create'
556
+ }
557
+
558
+ function isTableSchemaCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
559
+ return scope === 'admin' && command.commandPath[0] === 'table' && (command.commandPath[1] === 'create' || command.commandPath[1] === 'update-schema')
560
+ }
561
+
562
+ function isAccessRuleCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
563
+ return scope === 'admin' && command.commandPath[0] === 'access-rule'
564
+ }
565
+
566
+ function isAccessRuleUpdateCommand(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): boolean {
567
+ return scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update'
568
+ }
569
+
570
+ function buildAccessRuleShapeHint(userId = 2188889901) {
571
+ return buildHiddenFieldAccessRuleBody(userId)
572
+ }
573
+
574
+ function buildFullAccessRuleBody(userId = 2188889977) {
575
+ return {
576
+ name: 'Admin full access',
577
+ enabled: true,
578
+ type: 'form',
579
+ options: {
580
+ user_scope: [{ type: 'user', id: userId }],
581
+ policy: {
582
+ key: 'custom',
583
+ actions: ['view', 'add', 'edit', 'delete'],
584
+ all_fields_read: true,
585
+ all_fields_write: true,
586
+ },
587
+ list_options: {},
588
+ },
589
+ }
590
+ }
591
+
592
+ function buildHiddenFieldAccessRuleBody(userId = 2188889901) {
593
+ return {
594
+ name: 'Sales read write',
595
+ enabled: true,
596
+ type: 'form',
597
+ options: {
598
+ user_scope: [{ type: 'user', id: userId }],
599
+ policy: {
600
+ key: 'custom',
601
+ actions: ['view', 'add', 'edit'],
602
+ all_fields_read: true,
603
+ all_fields_write: true,
604
+ fields: [{ key: ':1002', read: false, write: false, report: false }],
605
+ },
606
+ list_options: {},
607
+ },
608
+ }
609
+ }
610
+
611
+ function buildAccessRuleCreateToolCall(body: unknown) {
612
+ return {
613
+ command: 'access-rule create',
614
+ args: ['--app-id', '<app_id>', '--table-id', '<table_id>', '--body-json', JSON.stringify(body)],
615
+ }
616
+ }
617
+
345
618
  function buildAssignUsersValidationDetails(
346
619
  scope: CommandScope,
347
620
  command: NonNullable<ReturnType<typeof findCommand>>,
348
621
  requestType: string,
349
622
  path: string,
350
623
  message: string,
624
+ env: EnvInput = process.env,
351
625
  ): CLIErrorDetails {
352
626
  return {
353
627
  operation: `${scope}.${command.module}.${command.functionName}`,
354
628
  requestType,
355
629
  issues: [{ path, message }],
356
630
  tsHints: [
357
- { symbol: 'AppIngressUpdateReqVO', file: '/opt/arcubase-sdk/types/ingress.ts' },
358
- { symbol: 'EntityIngressOptions', file: '/opt/arcubase-sdk/types/ingress.ts' },
359
- { symbol: 'PermitUserScope', file: '/opt/arcubase-sdk/types/common.ts' },
631
+ { symbol: 'AppIngressUpdateReqVO', file: sdkPath('types/ingress.ts', env) },
632
+ { symbol: 'EntityIngressOptions', file: sdkPath('types/ingress.ts', env) },
633
+ { symbol: 'PermitUserScope', file: sdkPath('types/common.ts', env) },
360
634
  ],
361
- docHints: getDocHints(requestType),
635
+ docHints: getDocHints(requestType, env),
362
636
  shapeHint: {
363
637
  update: ['user_scope'],
364
638
  options: {
365
- user_scope: [{ type: 'member', id: 123 }],
639
+ user_scope: [{ type: 'user', id: 2188889901 }],
366
640
  },
367
641
  },
368
642
  commonMistakes: [
@@ -374,33 +648,344 @@ function buildAssignUsersValidationDetails(
374
648
  }
375
649
  }
376
650
 
377
- function throwBodyValidationFailure(message: string, requestType: string, path: string, scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>): never {
378
- if (isAssignUsersCommand(scope, command)) {
379
- throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildAssignUsersValidationDetails(scope, command, requestType, path, message))
651
+ function buildBodyGuidance(requestType: string): Pick<CLIErrorDetails, 'retryToolCalls' | 'shapeHint' | 'commonMistakes' | 'suggestions'> {
652
+ if (requestType === 'EntitySaveReqVO' || requestType === 'AppCreateEntityReqVO') {
653
+ return {
654
+ shapeHint: {
655
+ id: 2188889845,
656
+ app_id: 2188889844,
657
+ name: '订单',
658
+ schema_version: 0,
659
+ field_id_seq: 1004,
660
+ layout: [[1001], [1002], [1003]],
661
+ fields: [
662
+ {
663
+ id: 1001,
664
+ label: '订单号',
665
+ type: 'text',
666
+ required: true,
667
+ unique: false,
668
+ editable: true,
669
+ visible: true,
670
+ show_label: true,
671
+ scannable: false,
672
+ default_value_mode: 0,
673
+ description: '',
674
+ value: null,
675
+ number_decimal: 0,
676
+ depends: [],
677
+ key: 'order_no',
678
+ code_index: false,
679
+ options: { type: 'text', placeholder: '', lengthLimit: false, lengthMin: 0, lengthMax: 255 },
680
+ transfers: null,
681
+ children: null,
682
+ },
683
+ {
684
+ id: 1002,
685
+ label: '描述',
686
+ type: 'textarea',
687
+ required: false,
688
+ unique: false,
689
+ editable: true,
690
+ visible: true,
691
+ show_label: true,
692
+ scannable: false,
693
+ default_value_mode: 0,
694
+ description: '',
695
+ value: null,
696
+ number_decimal: 0,
697
+ depends: [],
698
+ key: 'description',
699
+ code_index: false,
700
+ options: { placeholder: '', html: false, size: 'default', lengthLimit: false, lengthMin: 0, lengthMax: 2000 },
701
+ transfers: null,
702
+ children: null,
703
+ },
704
+ {
705
+ id: 1003,
706
+ label: '状态',
707
+ type: 'select',
708
+ required: true,
709
+ unique: false,
710
+ editable: true,
711
+ visible: true,
712
+ show_label: true,
713
+ scannable: false,
714
+ default_value_mode: 1,
715
+ description: '',
716
+ value: 'draft',
717
+ number_decimal: 0,
718
+ depends: [],
719
+ key: 'status',
720
+ code_index: false,
721
+ options: {
722
+ horizontal: false,
723
+ numberMax: '',
724
+ numberMin: '',
725
+ numberRange: false,
726
+ repeatRemind: 'This value already exists and duplicate submission is not allowed',
727
+ options: {
728
+ mode: 'custom',
729
+ colorful: false,
730
+ items: [
731
+ { key: 1, value: 'draft' },
732
+ { key: 2, value: 'active' },
733
+ ],
734
+ },
735
+ },
736
+ transfers: null,
737
+ children: null,
738
+ },
739
+ ],
740
+ options: { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
741
+ workflow_enabled: false,
742
+ },
743
+ commonMistakes: [
744
+ 'table create must include fields and field_id_seq; do not create an empty table shell',
745
+ 'prefer --body-json for table create/update-schema; do not create a body file unless JSON is too large for one tool call',
746
+ 'fields must be an array of full FieldVO objects, not an object map',
747
+ 'get table shell first: arcubase-admin table get --app-id <app_id> --table-id <table_id>',
748
+ 'save the full returned schema after editing; do not submit only changed fields',
749
+ 'field_id_seq must be greater than every field.id',
750
+ 'layout must reference existing field ids',
751
+ 'use type "text", not "string"',
752
+ 'use type "member", not "user"',
753
+ 'for select/radio/checkbox/status, options must contain options.items',
754
+ 'for linkto/relation/query, options must contain relation.app_id, relation.entity_id, relation.type',
755
+ 'do not put per-user permissions in FieldVO; use access-rule for read/write and hidden fields',
756
+ ],
757
+ suggestions: [
758
+ `retry with: arcubase-admin table create --app-id <app_id> --body-json '${JSON.stringify(buildTableCreateExampleBody())}'`,
759
+ ],
760
+ }
761
+ }
762
+ if (requestType === 'AppIngressCreateReqVO' || requestType === 'AppIngressUpdateReqVO') {
763
+ const retryToolCalls = [
764
+ buildAccessRuleCreateToolCall(buildFullAccessRuleBody()),
765
+ buildAccessRuleCreateToolCall(buildHiddenFieldAccessRuleBody()),
766
+ ]
767
+ return {
768
+ retryToolCalls,
769
+ suggestions: [
770
+ 'next arcubase-admin-cli input: retryToolCalls[0] for full access or retryToolCalls[1] for hidden-field access',
771
+ ],
772
+ shapeHint: buildAccessRuleShapeHint(),
773
+ commonMistakes: [
774
+ 'use options.policy, not top-level permissions',
775
+ 'use options.policy.fields, not field_visibility',
776
+ 'actions must use Arcubase action codes: view, add, edit; do not use read, write, or insert',
777
+ 'field permission keys must be strings prefixed with colon, for example ":1002"; do not use FieldVO.key values such as "vote_name"',
778
+ 'for access-rule update policy changes, use {"update":["options"],"options":{"policy":...,"list_options":{}}}; do not use update:["options.policy"]',
779
+ 'use options.user_scope with type user/department/actor and numeric id; do not use FieldVO type "member" here',
780
+ 'use assign-users only for updating user_scope later',
781
+ 'assign-users preserves existing policy when body.update=["user_scope"]',
782
+ ],
783
+ }
784
+ }
785
+ if (requestType === 'InsertReqVO') {
786
+ return {
787
+ shapeHint: {
788
+ fields: {
789
+ '1001': 'SO-1001',
790
+ '1002': 199,
791
+ },
792
+ },
793
+ commonMistakes: [
794
+ 'row fields must use numeric field ids as JSON object keys',
795
+ 'do not use field labels such as "标题" or "投票标题" as keys',
796
+ 'run table get first when field ids are unclear',
797
+ 'do not add access policy fields; the CLI resolves access policy internally',
798
+ ],
799
+ suggestions: [
800
+ 'run: arcubase table get --app-id <app_id> --table-id <table_id>',
801
+ 'retry with: arcubase row create --app-id <app_id> --table-id <table_id> --body-json \'{"fields":{"1001":"今天吃什么"}}\'',
802
+ ],
803
+ }
804
+ }
805
+ if (requestType === 'EntityQueryReqVO') {
806
+ return {
807
+ shapeHint: {
808
+ limit: 20,
809
+ offset: 0,
810
+ search: { text_search: 'SO-1001' },
811
+ },
812
+ commonMistakes: [
813
+ 'limit, offset, search, sorts, and view_mode belong inside --body-json',
814
+ 'do not pass --limit or --offset flags',
815
+ 'do not add access policy fields; the CLI resolves access policy internally',
816
+ ],
817
+ suggestions: [
818
+ 'retry with: arcubase row query --app-id <app_id> --table-id <table_id> --body-json \'{"limit":20,"offset":0}\'',
819
+ ],
820
+ }
821
+ }
822
+ if (requestType === 'EntityUpdateReqVO') {
823
+ return {
824
+ shapeHint: {
825
+ data: { '1002': 299 },
826
+ changed_fields: [1002],
827
+ },
828
+ commonMistakes: [
829
+ 'use data, not fields',
830
+ 'changed_fields is required and must match data keys',
831
+ 'do not add access policy fields; the CLI resolves access policy internally',
832
+ ],
833
+ }
834
+ }
835
+ if (requestType === 'EntityBulkUpdateReqVO') {
836
+ return {
837
+ shapeHint: {
838
+ selection: {
839
+ type: 'ids',
840
+ ids: [2188890411, 2188890412],
841
+ length: 2,
842
+ },
843
+ fields: [
844
+ {
845
+ id: 1003,
846
+ action: {
847
+ type: 'set',
848
+ value: 2,
849
+ },
850
+ },
851
+ ],
852
+ },
853
+ commonMistakes: [
854
+ 'bulk update fields[].action must be an object with type',
855
+ 'use {"type":"set","value":2} to assign a scalar value',
856
+ 'do not use value_set, setValue, or a top-level value field',
857
+ 'use {"type":"number_add","number_add":10} only for numeric increments',
858
+ 'selection.type=ids requires ids and length',
859
+ ],
860
+ suggestions: [
861
+ 'retry with: arcubase row bulk-update --app-id <app_id> --table-id <table_id> --body-json \'{"selection":{"type":"ids","ids":[2188890411],"length":1},"fields":[{"id":1003,"action":{"type":"set","value":2}}]}\'',
862
+ ],
863
+ }
380
864
  }
381
- throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
865
+ return {}
866
+ }
867
+
868
+ function buildBodyValidationDetails(
869
+ scope: CommandScope,
870
+ command: NonNullable<ReturnType<typeof findCommand>>,
871
+ requestType: string,
872
+ issues: CLIErrorDetails['issues'],
873
+ env: EnvInput = process.env,
874
+ ): CLIErrorDetails {
875
+ return {
382
876
  operation: `${scope}.${command.module}.${command.functionName}`,
383
877
  requestType,
384
- issues: [
385
- {
386
- path,
387
- message,
388
- },
389
- ],
390
- tsHints: getTypeHints(requestType),
391
- docHints: getDocHints(requestType),
392
- })
878
+ issues,
879
+ tsHints: getTypeHints(requestType, env),
880
+ docHints: getDocHints(requestType, env),
881
+ ...buildBodyGuidance(requestType),
882
+ }
883
+ }
884
+
885
+ function throwBodyValidationFailure(message: string, requestType: string, path: string, scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>, env: EnvInput = process.env): never {
886
+ if (isAssignUsersCommand(scope, command)) {
887
+ throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildAssignUsersValidationDetails(scope, command, requestType, path, message, env))
888
+ }
889
+ throw new CLIError('BODY_VALIDATION_FAILED', message, 2, buildBodyValidationDetails(scope, command, requestType, [{ path, message }], env))
890
+ }
891
+
892
+ function requireTableCreateSchemaBody(scope: CommandScope, command: NonNullable<ReturnType<typeof findCommand>>, body: unknown, env: EnvInput) {
893
+ if (!isRecord(body)) {
894
+ throwBodyValidationFailure(
895
+ 'table create requires a JSON object with name, fields, field_id_seq, layout, options, and workflow_enabled',
896
+ 'AppCreateEntityReqVO',
897
+ 'body',
898
+ scope,
899
+ command,
900
+ env,
901
+ )
902
+ }
903
+ if (!Array.isArray(body.fields) || body.fields.length === 0) {
904
+ throwBodyValidationFailure(
905
+ 'table create must include non-empty body.fields; empty table shell creation is not allowed',
906
+ 'AppCreateEntityReqVO',
907
+ 'body.fields',
908
+ scope,
909
+ command,
910
+ env,
911
+ )
912
+ }
913
+ if (typeof body.field_id_seq !== 'number') {
914
+ throwBodyValidationFailure(
915
+ 'table create must include body.field_id_seq greater than every field.id',
916
+ 'AppCreateEntityReqVO',
917
+ 'body.field_id_seq',
918
+ scope,
919
+ command,
920
+ env,
921
+ )
922
+ }
393
923
  }
394
924
 
395
925
  function stringArray(value: unknown): string[] {
396
926
  return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
397
927
  }
398
928
 
929
+ function validateAccessRuleUserScope(
930
+ scope: CommandScope,
931
+ command: NonNullable<ReturnType<typeof findCommand>>,
932
+ requestType: string,
933
+ userScope: unknown,
934
+ env: EnvInput,
935
+ pathPrefix = 'body.options.user_scope',
936
+ ) {
937
+ if (userScope === undefined) return
938
+ if (!Array.isArray(userScope)) {
939
+ throwBodyValidationFailure(
940
+ 'access-rule user_scope must be an array of {"type":"user","id":2188889901}',
941
+ requestType,
942
+ pathPrefix,
943
+ scope,
944
+ command,
945
+ env,
946
+ )
947
+ }
948
+ const allowedTypes = new Set(['user', 'department', 'actor'])
949
+ for (const [index, item] of userScope.entries()) {
950
+ if (!isRecord(item)) {
951
+ throwBodyValidationFailure(
952
+ 'access-rule user_scope item must be an object',
953
+ requestType,
954
+ `${pathPrefix}.${index}`,
955
+ scope,
956
+ command,
957
+ env,
958
+ )
959
+ }
960
+ if (typeof item.type !== 'string' || !allowedTypes.has(item.type)) {
961
+ throwBodyValidationFailure(
962
+ 'access-rule user_scope[].type must be "user", "department", or "actor"; do not use FieldVO type "member" here',
963
+ requestType,
964
+ `${pathPrefix}.${index}.type`,
965
+ scope,
966
+ command,
967
+ env,
968
+ )
969
+ }
970
+ if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
971
+ throwBodyValidationFailure(
972
+ 'access-rule user_scope[].id must be numeric TenantUser.ID, department ID, or actor ID',
973
+ requestType,
974
+ `${pathPrefix}.${index}.id`,
975
+ scope,
976
+ command,
977
+ env,
978
+ )
979
+ }
980
+ }
981
+ }
982
+
399
983
  function requireUpdateContains(
400
984
  scope: CommandScope,
401
985
  command: NonNullable<ReturnType<typeof findCommand>>,
402
986
  body: Record<string, unknown>,
403
987
  field: string,
988
+ env: EnvInput,
404
989
  ) {
405
990
  if (!(field in body)) return
406
991
  const update = stringArray(body.update)
@@ -411,6 +996,7 @@ function requireUpdateContains(
411
996
  'body.update',
412
997
  scope,
413
998
  command,
999
+ env,
414
1000
  )
415
1001
  }
416
1002
  }
@@ -419,6 +1005,7 @@ function validateActionSpecificRawBody(
419
1005
  scope: CommandScope,
420
1006
  command: NonNullable<ReturnType<typeof findCommand>>,
421
1007
  body: unknown,
1008
+ env: EnvInput,
422
1009
  ) {
423
1010
  if (!isRecord(body) || !command.requestType) {
424
1011
  return
@@ -427,13 +1014,112 @@ function validateActionSpecificRawBody(
427
1014
  if (isAssignUsersCommand(scope, command)) {
428
1015
  if ('users' in body || 'allowedUsers' in body || 'user_scope' in body) {
429
1016
  throwBodyValidationFailure(
430
- 'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
1017
+ 'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
431
1018
  command.requestType,
432
1019
  'body.options.user_scope',
433
1020
  scope,
434
1021
  command,
1022
+ env,
1023
+ )
1024
+ }
1025
+ }
1026
+
1027
+ if (isTableSchemaCommand(scope, command)) {
1028
+ const fields = Array.isArray(body.fields) ? body.fields : []
1029
+ const hasPermissionPatch = fields.some((field) => isRecord(field) && ('permission' in field || 'permissions' in field || 'field_visibility' in field))
1030
+ if (hasPermissionPatch) {
1031
+ throwBodyValidationFailure(
1032
+ 'table schema cannot configure per-user permissions. Use access-rule create/update with options.policy.fields for hidden fields',
1033
+ command.requestType,
1034
+ 'body.fields',
1035
+ scope,
1036
+ command,
1037
+ env,
1038
+ )
1039
+ }
1040
+ }
1041
+
1042
+ if (isAccessRuleCommand(scope, command)) {
1043
+ const update = stringArray(body.update)
1044
+ if (isAccessRuleUpdateCommand(scope, command) && update.some((item) => item.startsWith('options.'))) {
1045
+ throwBodyValidationFailure(
1046
+ 'access-rule update cannot patch nested options paths. Use {"update":["options"],"options":{"policy":{"key":"custom","actions":["view","add","edit"],"all_fields_read":true,"all_fields_write":true,"fields":[{"key":":1002","read":false,"write":false,"report":false}]},"list_options":{}}}',
1047
+ command.requestType,
1048
+ 'body.update',
1049
+ scope,
1050
+ command,
1051
+ env,
1052
+ )
1053
+ }
1054
+ if ('permissions' in body || 'field_visibility' in body || 'description' in body) {
1055
+ throwBodyValidationFailure(
1056
+ 'access-rule body only supports name, enabled, type, and options; use options.policy.actions and options.policy.fields',
1057
+ command.requestType,
1058
+ 'body',
1059
+ scope,
1060
+ command,
1061
+ env,
1062
+ )
1063
+ }
1064
+ const options = isRecord(body.options) ? body.options : undefined
1065
+ if (options && ('permissions' in options || 'field_visibility' in options)) {
1066
+ throwBodyValidationFailure(
1067
+ 'access-rule options only supports user_scope, policy, list_options, i18n_name; use options.policy.actions and options.policy.fields',
1068
+ command.requestType,
1069
+ 'body.options',
1070
+ scope,
1071
+ command,
1072
+ env,
1073
+ )
1074
+ }
1075
+ const policy = options && isRecord(options.policy) ? options.policy : undefined
1076
+ if (isAccessRuleUpdateCommand(scope, command) && update.includes('options') && options?.user_scope !== undefined && !policy) {
1077
+ throwBodyValidationFailure(
1078
+ 'access-rule update with update:["options"] cannot change only options.user_scope because it overwrites policy. Use access-rule assign-users for user assignment, or include the full options.policy and options.list_options.',
1079
+ command.requestType,
1080
+ 'body.options.policy',
1081
+ scope,
1082
+ command,
1083
+ env,
1084
+ )
1085
+ }
1086
+ validateAccessRuleUserScope(scope, command, command.requestType, options?.user_scope, env)
1087
+ const actions = policy && Array.isArray(policy.actions) ? policy.actions : []
1088
+ if (actions.includes('read') || actions.includes('write')) {
1089
+ throwBodyValidationFailure(
1090
+ 'access-rule policy.actions must use Arcubase action codes: use "view" for read and "add","edit" for write; do not use "read", "write", or "insert"',
1091
+ command.requestType,
1092
+ 'body.options.policy.actions',
1093
+ scope,
1094
+ command,
1095
+ env,
435
1096
  )
436
1097
  }
1098
+ const fields = policy && Array.isArray(policy.fields) ? policy.fields : []
1099
+ const allowedSystemFieldKeys = new Set(['created', 'updated', 'creator'])
1100
+ for (const [index, field] of fields.entries()) {
1101
+ if (!isRecord(field)) continue
1102
+ if (typeof field.key === 'number' || (typeof field.key === 'string' && /^\d+$/.test(field.key))) {
1103
+ throwBodyValidationFailure(
1104
+ 'access-rule policy.fields[].key must be a string prefixed with colon, for example ":1002"; do not use raw numbers',
1105
+ command.requestType,
1106
+ `body.options.policy.fields.${index}.key`,
1107
+ scope,
1108
+ command,
1109
+ env,
1110
+ )
1111
+ }
1112
+ if (typeof field.key === 'string' && !field.key.startsWith(':') && !allowedSystemFieldKeys.has(field.key)) {
1113
+ throwBodyValidationFailure(
1114
+ 'access-rule policy.fields[].key must use Arcubase permission keys such as ":1002"; do not use FieldVO.key values such as "vote_name"',
1115
+ command.requestType,
1116
+ `body.options.policy.fields.${index}.key`,
1117
+ scope,
1118
+ command,
1119
+ env,
1120
+ )
1121
+ }
1122
+ }
437
1123
  }
438
1124
  }
439
1125
 
@@ -442,15 +1128,16 @@ function validateActionSpecificBody(
442
1128
  command: NonNullable<ReturnType<typeof findCommand>>,
443
1129
  flags: Record<string, string | boolean>,
444
1130
  body: unknown,
1131
+ env: EnvInput,
445
1132
  ) {
446
1133
  if (!isRecord(body) || !command.requestType) {
447
1134
  return
448
1135
  }
449
1136
 
450
1137
  if (scope === 'admin' && command.commandPath[0] === 'access-rule' && command.commandPath[1] === 'update') {
451
- requireUpdateContains(scope, command, body, 'enabled')
452
- requireUpdateContains(scope, command, body, 'name')
453
- requireUpdateContains(scope, command, body, 'options')
1138
+ requireUpdateContains(scope, command, body, 'enabled', env)
1139
+ requireUpdateContains(scope, command, body, 'name', env)
1140
+ requireUpdateContains(scope, command, body, 'options', env)
454
1141
  }
455
1142
 
456
1143
  if (isAssignUsersCommand(scope, command)) {
@@ -459,11 +1146,12 @@ function validateActionSpecificBody(
459
1146
  const userScope = options && Array.isArray(options.user_scope) ? options.user_scope : undefined
460
1147
  if ('users' in body || 'allowedUsers' in body) {
461
1148
  throwBodyValidationFailure(
462
- 'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"member","id":123}]}}',
1149
+ 'access-rule assign-users only accepts body.options.user_scope with {"update":["user_scope"],"options":{"user_scope":[{"type":"user","id":2188889901}]}}',
463
1150
  command.requestType,
464
1151
  'body.options.user_scope',
465
1152
  scope,
466
1153
  command,
1154
+ env,
467
1155
  )
468
1156
  }
469
1157
  if (!update.includes('user_scope')) {
@@ -473,6 +1161,7 @@ function validateActionSpecificBody(
473
1161
  'body.update',
474
1162
  scope,
475
1163
  command,
1164
+ env,
476
1165
  )
477
1166
  }
478
1167
  if (!userScope || userScope.length === 0) {
@@ -482,8 +1171,10 @@ function validateActionSpecificBody(
482
1171
  'body.options.user_scope',
483
1172
  scope,
484
1173
  command,
1174
+ env,
485
1175
  )
486
1176
  }
1177
+ validateAccessRuleUserScope(scope, command, command.requestType, userScope, env)
487
1178
  const assignedUserScope = userScope
488
1179
  for (const [index, item] of assignedUserScope.entries()) {
489
1180
  if (!isRecord(item)) {
@@ -493,15 +1184,17 @@ function validateActionSpecificBody(
493
1184
  `body.options.user_scope.${index}`,
494
1185
  scope,
495
1186
  command,
1187
+ env,
496
1188
  )
497
1189
  }
498
- if (item.type !== 'member') {
1190
+ if (item.type !== 'user') {
499
1191
  throwBodyValidationFailure(
500
- 'access-rule assign-users requires body.options.user_scope[].type to be "member"',
1192
+ 'access-rule assign-users requires body.options.user_scope[].type to be "user"',
501
1193
  command.requestType,
502
1194
  `body.options.user_scope.${index}.type`,
503
1195
  scope,
504
1196
  command,
1197
+ env,
505
1198
  )
506
1199
  }
507
1200
  if (typeof item.id !== 'number' || !Number.isInteger(item.id) || item.id <= 0) {
@@ -511,6 +1204,7 @@ function validateActionSpecificBody(
511
1204
  `body.options.user_scope.${index}.id`,
512
1205
  scope,
513
1206
  command,
1207
+ env,
514
1208
  )
515
1209
  }
516
1210
  }
@@ -535,6 +1229,7 @@ function validateActionSpecificBody(
535
1229
  'body.selection.type',
536
1230
  scope,
537
1231
  command,
1232
+ env,
538
1233
  )
539
1234
  }
540
1235
  return
@@ -547,6 +1242,7 @@ function validateActionSpecificBody(
547
1242
  'body.selection.type',
548
1243
  scope,
549
1244
  command,
1245
+ env,
550
1246
  )
551
1247
  }
552
1248
 
@@ -557,11 +1253,436 @@ function validateActionSpecificBody(
557
1253
  'body.selection.condition',
558
1254
  scope,
559
1255
  command,
1256
+ env,
560
1257
  )
561
1258
  }
562
1259
  }
563
1260
  }
564
1261
 
1262
+ function unwrapData(value: unknown): unknown {
1263
+ let current = value
1264
+ for (let i = 0; i < 3; i += 1) {
1265
+ if (isRecord(current) && isRecord(current.data)) {
1266
+ current = current.data
1267
+ continue
1268
+ }
1269
+ break
1270
+ }
1271
+ return current
1272
+ }
1273
+
1274
+ function numericFieldKeys(fields: unknown): string[] {
1275
+ if (!isRecord(fields)) return []
1276
+ return Object.keys(fields).filter((key) => /^\d+$/.test(key)).sort((left, right) => Number(left) - Number(right))
1277
+ }
1278
+
1279
+ function permitFieldId(permit: unknown): string | undefined {
1280
+ if (!isRecord(permit) || typeof permit.key !== 'string') return undefined
1281
+ const match = /^:(\d+)$/.exec(permit.key)
1282
+ return match?.[1]
1283
+ }
1284
+
1285
+ function rowFieldPermits(value: unknown): Array<Record<string, unknown>> {
1286
+ const data = unwrapData(value)
1287
+ const fieldPermits = isRecord(data) && isRecord(data.fieldPermits) ? data.fieldPermits : undefined
1288
+ return fieldPermits && Array.isArray(fieldPermits.fields)
1289
+ ? fieldPermits.fields.filter((item): item is Record<string, unknown> => isRecord(item))
1290
+ : []
1291
+ }
1292
+
1293
+ function buildRowValueEvidence(command: NonNullable<ReturnType<typeof findCommand>>, responseBody: unknown): Record<string, unknown> | undefined {
1294
+ if (command.scope !== 'user' || command.commandPath[0] !== 'row') return undefined
1295
+ const action = command.commandPath[1]
1296
+ const data = unwrapData(responseBody)
1297
+ const rules = [
1298
+ 'current row values come only from row query fields or row get record.fields',
1299
+ 'table schema fields[].value and default_value_mode are schema defaults, not returned row values',
1300
+ 'a field id absent from returned row fields has no row-value evidence',
1301
+ 'read visibility status is fieldPermits read:false; FIELD_PERMISSION_REQUIRED is only a write preflight error',
1302
+ ]
1303
+ if (action === 'get') {
1304
+ const record = isRecord(data) && isRecord(data.record) ? data.record : undefined
1305
+ const returnedFieldIds = numericFieldKeys(record?.fields)
1306
+ const permits = rowFieldPermits(responseBody)
1307
+ const permitFieldIds = permits.map(permitFieldId).filter((id): id is string => Boolean(id))
1308
+ const deniedReadFieldIds = permits
1309
+ .filter((permit) => permit.read === false)
1310
+ .map(permitFieldId)
1311
+ .filter((id): id is string => Boolean(id))
1312
+ .sort((left, right) => Number(left) - Number(right))
1313
+ const notReturnedFieldIds = permitFieldIds
1314
+ .filter((id) => !returnedFieldIds.includes(id))
1315
+ .sort((left, right) => Number(left) - Number(right))
1316
+ return {
1317
+ source: 'row get record.fields',
1318
+ returnedFieldIds,
1319
+ notReturnedFieldIds,
1320
+ deniedReadFieldIds,
1321
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
1322
+ rules,
1323
+ }
1324
+ }
1325
+ if (action === 'query') {
1326
+ const items = isRecord(data) && Array.isArray(data.items) ? data.items : []
1327
+ return {
1328
+ source: 'row query items[].fields',
1329
+ rows: items.filter((item): item is Record<string, unknown> => isRecord(item)).map((item) => ({
1330
+ rowId: item.id,
1331
+ returnedFieldIds: numericFieldKeys(item.fields),
1332
+ })),
1333
+ absentFieldMeaning: 'not returned to current user; no row-value evidence',
1334
+ rules,
1335
+ }
1336
+ }
1337
+ return undefined
1338
+ }
1339
+
1340
+ function requiredRowAction(command: NonNullable<ReturnType<typeof findCommand>>, flags: Record<string, string | boolean>): string | undefined {
1341
+ if (command.commandPath[0] !== 'row') return undefined
1342
+ switch (command.commandPath[1]) {
1343
+ case 'query':
1344
+ case 'get':
1345
+ return 'view'
1346
+ case 'create':
1347
+ return 'add'
1348
+ case 'update':
1349
+ return 'edit'
1350
+ case 'delete':
1351
+ return 'delete'
1352
+ case 'bulk-update':
1353
+ return 'bulk_update'
1354
+ case 'selection-action':
1355
+ return flagValue(flags, 'action') || undefined
1356
+ default:
1357
+ return undefined
1358
+ }
1359
+ }
1360
+
1361
+ function rowCommandNeedsPolicy(command: NonNullable<ReturnType<typeof findCommand>>): boolean {
1362
+ return command.scope === 'user' && command.commandPath[0] === 'row'
1363
+ }
1364
+
1365
+ function extractIngressItems(appDetail: unknown): Array<{ id: string }> {
1366
+ const appData = unwrapData(appDetail)
1367
+ const ingress = isRecord(appData) ? appData.ingress : undefined
1368
+ const groups = isRecord(ingress) && Array.isArray(ingress.groups) ? ingress.groups : []
1369
+ const items: Array<{ id: string }> = []
1370
+ for (const group of groups) {
1371
+ const groupItems = isRecord(group) && Array.isArray(group.items) ? group.items : []
1372
+ for (const item of groupItems) {
1373
+ if (!isRecord(item)) continue
1374
+ const id = typeof item.id === 'string' ? item.id : typeof item.ID === 'string' ? item.ID : ''
1375
+ if (id) {
1376
+ items.push({ id })
1377
+ }
1378
+ }
1379
+ }
1380
+ return items
1381
+ }
1382
+
1383
+ type ResolvedRowPolicy = {
1384
+ policyID: string
1385
+ policy?: Record<string, unknown>
1386
+ }
1387
+
1388
+ function ingressMatchesTableAndAction(ingressDetail: unknown, tableId: string, action: string | undefined): ResolvedRowPolicy | undefined {
1389
+ const ingressData = unwrapData(ingressDetail)
1390
+ if (!isRecord(ingressData)) return undefined
1391
+ const id = typeof ingressData.id === 'number' || typeof ingressData.id === 'string'
1392
+ ? ingressData.id
1393
+ : typeof ingressData.ID === 'number' || typeof ingressData.ID === 'string'
1394
+ ? ingressData.ID
1395
+ : undefined
1396
+ if (String(id ?? '') !== tableId) return undefined
1397
+ const policyID = typeof ingressData.hash_id === 'string'
1398
+ ? ingressData.hash_id
1399
+ : typeof ingressData.hashId === 'string'
1400
+ ? ingressData.hashId
1401
+ : undefined
1402
+ const policy = isRecord(ingressData.policy) ? ingressData.policy : undefined
1403
+ if (!action) return { policyID: policyID || '', policy }
1404
+ const actions = policy && Array.isArray(policy.actions) ? policy.actions.filter((item): item is string => typeof item === 'string') : []
1405
+ if (!actions.includes(action)) return undefined
1406
+ return { policyID: policyID || '', policy }
1407
+ }
1408
+
1409
+ function writtenRowFieldIds(command: NonNullable<ReturnType<typeof findCommand>>, body: unknown): number[] {
1410
+ if (!isRecord(body)) return []
1411
+ const source = command.commandPath[1] === 'create'
1412
+ ? body.fields
1413
+ : command.commandPath[1] === 'update'
1414
+ ? body.data
1415
+ : undefined
1416
+ if (!isRecord(source)) return []
1417
+ return Object.keys(source)
1418
+ .filter((key) => /^\d+$/.test(key))
1419
+ .map((key) => Number(key))
1420
+ }
1421
+
1422
+ function fieldWriteAllowedByPolicy(policy: Record<string, unknown> | undefined, fieldId: number): boolean {
1423
+ if (!policy) return true
1424
+ const fields = Array.isArray(policy.fields) ? policy.fields : []
1425
+ const explicit = fields.find((item) => isRecord(item) && item.key === `:${fieldId}`)
1426
+ if (isRecord(explicit) && explicit.write === false) return false
1427
+ if (isRecord(explicit) && explicit.write === true) return true
1428
+ if (policy.all_fields_write === false) return false
1429
+ return true
1430
+ }
1431
+
1432
+ function assertWritableFieldsForRowCommand(
1433
+ command: NonNullable<ReturnType<typeof findCommand>>,
1434
+ runtimeEnv: RuntimeEnv,
1435
+ body: unknown,
1436
+ policy: Record<string, unknown> | undefined,
1437
+ ) {
1438
+ if (command.commandPath[1] !== 'create' && command.commandPath[1] !== 'update') {
1439
+ return
1440
+ }
1441
+ const denied = writtenRowFieldIds(command, body).filter((fieldId) => !fieldWriteAllowedByPolicy(policy, fieldId))
1442
+ if (denied.length === 0) {
1443
+ return
1444
+ }
1445
+ const payloadKey = command.commandPath[1] === 'create' ? 'fields' : 'data'
1446
+ throw new CLIError('FIELD_PERMISSION_REQUIRED', `row ${command.commandPath[1]} includes field ids without write permission: ${denied.join(', ')}`, 2, {
1447
+ operation: `user.${command.module}.${command.functionName}`,
1448
+ requestType: command.requestType ?? undefined,
1449
+ issues: denied.map((fieldId) => ({
1450
+ path: `body.${payloadKey}.${fieldId}`,
1451
+ message: 'field is not writable for the resolved Arcubase access rule',
1452
+ })),
1453
+ docHints: [
1454
+ { title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
1455
+ { title: 'Runtime reference', file: sdkPath('docs/runtime-reference/README.md', runtimeEnv) },
1456
+ ],
1457
+ suggestions: [
1458
+ 'remove non-writable field ids from the row body and retry',
1459
+ 'run arcubase table get to map visible business fields to numeric field ids',
1460
+ 'ask an admin to update the access-rule if this field should be writable',
1461
+ ],
1462
+ })
1463
+ }
1464
+
1465
+ async function resolvePolicyIDForRowCommand(
1466
+ command: NonNullable<ReturnType<typeof findCommand>>,
1467
+ runtimeEnv: RuntimeEnv,
1468
+ flags: Record<string, string | boolean>,
1469
+ fetchImpl: typeof fetch,
1470
+ ): Promise<ResolvedRowPolicy> {
1471
+ const appId = flagValue(flags, 'app-id')
1472
+ const tableId = flagValue(flags, 'table-id')
1473
+ const action = requiredRowAction(command, flags)
1474
+ if (!appId || !tableId) {
1475
+ throw new CLIError('POLICY_ID_RESOLUTION_FAILED', 'row command requires --app-id and --table-id to resolve Arcubase policy_id', 2, {
1476
+ operation: `user.${command.module}.${command.functionName}`,
1477
+ docHints: getDocHints(command.requestType ?? '', runtimeEnv),
1478
+ })
1479
+ }
1480
+
1481
+ const appDetail = await requestJSON(runtimeEnv, 'GET', `/api/apps/${encodeURIComponent(appId)}`, undefined, fetchImpl)
1482
+ const ingressItems = extractIngressItems(appDetail.data)
1483
+ for (const item of ingressItems) {
1484
+ const ingressDetail = await requestJSON(runtimeEnv, 'GET', `/api/ingress/${encodeURIComponent(appId)}/${encodeURIComponent(item.id)}`, undefined, fetchImpl)
1485
+ const match = ingressMatchesTableAndAction(ingressDetail.data, tableId, action)
1486
+ if (match) {
1487
+ return { ...match, policyID: match.policyID || item.id }
1488
+ }
1489
+ }
1490
+
1491
+ throw new CLIError('POLICY_ID_RESOLUTION_FAILED', `no accessible ingress policy found for app ${appId}, table ${tableId}${action ? `, action ${action}` : ''}`, 2, {
1492
+ operation: `user.${command.module}.${command.functionName}`,
1493
+ requestType: command.requestType ?? undefined,
1494
+ issues: [{ path: 'policy_id', message: 'CLI could not resolve policy_id from accessible ingress' }],
1495
+ docHints: [
1496
+ { title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
1497
+ { title: 'Runtime reference', file: sdkPath('docs/runtime-reference/README.md', runtimeEnv) },
1498
+ ],
1499
+ suggestions: [
1500
+ 'run arcubase entry and confirm the app/table is visible',
1501
+ 'ask an admin to create an enabled access-rule with the required action for this table',
1502
+ ],
1503
+ })
1504
+ }
1505
+
1506
+ async function injectResolvedPolicyID(
1507
+ command: NonNullable<ReturnType<typeof findCommand>>,
1508
+ runtimeEnv: RuntimeEnv,
1509
+ flags: Record<string, string | boolean>,
1510
+ query: Record<string, unknown>,
1511
+ body: unknown,
1512
+ fetchImpl: typeof fetch,
1513
+ ): Promise<{ query: Record<string, unknown>; body: unknown }> {
1514
+ if (!rowCommandNeedsPolicy(command)) {
1515
+ return { query, body }
1516
+ }
1517
+ if (typeof query.policy_id === 'string' && query.policy_id) {
1518
+ return { query, body }
1519
+ }
1520
+ if (isRecord(body) && typeof body.policy_id === 'string' && body.policy_id) {
1521
+ return { query, body }
1522
+ }
1523
+
1524
+ const resolvedPolicy = await resolvePolicyIDForRowCommand(command, runtimeEnv, flags, fetchImpl)
1525
+ const policyID = resolvedPolicy.policyID
1526
+ assertWritableFieldsForRowCommand(command, runtimeEnv, body, resolvedPolicy.policy)
1527
+ if (command.commandPath[1] === 'get') {
1528
+ return { query: { ...query, policy_id: policyID }, body }
1529
+ }
1530
+ if (isRecord(body)) {
1531
+ return { query, body: { ...body, policy_id: policyID } }
1532
+ }
1533
+ return { query, body }
1534
+ }
1535
+
1536
+ function assertNoUserPolicyIDInput(
1537
+ command: NonNullable<ReturnType<typeof findCommand>>,
1538
+ runtimeEnv: RuntimeEnv,
1539
+ query: Record<string, unknown>,
1540
+ body: unknown,
1541
+ ) {
1542
+ if (!rowCommandNeedsPolicy(command)) {
1543
+ return
1544
+ }
1545
+ if ('policy_id' in query || (isRecord(body) && 'policy_id' in body)) {
1546
+ throw new CLIError('INTERNAL_POLICY_ID_NOT_ALLOWED', 'policy_id is an internal Arcubase ingress parameter; use app/table/row ids only', 2, {
1547
+ operation: `user.${command.module}.${command.functionName}`,
1548
+ issues: [{ path: 'policy_id', message: 'do not provide policy_id in flags, query file, or body JSON' }],
1549
+ docHints: [
1550
+ { title: 'Row CRUD', file: sdkPath('docs/runtime-reference/row-crud.md', runtimeEnv) },
1551
+ { title: 'Runtime reference', file: sdkPath('docs/runtime-reference/README.md', runtimeEnv) },
1552
+ ],
1553
+ suggestions: ['run arcubase entry to find the app/table ids, then retry without policy_id'],
1554
+ })
1555
+ }
1556
+ }
1557
+
1558
+ async function requestJSON(
1559
+ runtimeEnv: RuntimeEnv,
1560
+ method: string,
1561
+ endpoint: string,
1562
+ body: unknown,
1563
+ fetchImpl: typeof fetch,
1564
+ ): Promise<{ status: number; data: any }> {
1565
+ const response = await fetchUpstream(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint), {
1566
+ method,
1567
+ headers: {
1568
+ ...buildRequestHeaders(runtimeEnv),
1569
+ 'Content-Type': 'application/json',
1570
+ },
1571
+ body: body === undefined || method === 'GET' ? undefined : JSON.stringify(body),
1572
+ }, endpoint, method, fetchImpl)
1573
+ const raw = await response.text()
1574
+ let parsedResponse: any = raw
1575
+ try {
1576
+ parsedResponse = raw ? JSON.parse(raw) : null
1577
+ } catch {
1578
+ // keep raw text
1579
+ }
1580
+ if (!response.ok) {
1581
+ throwUpstreamHTTPError(endpoint, method, response, parsedResponse)
1582
+ }
1583
+ if (parsedResponse && typeof parsedResponse === 'object' && 'error' in parsedResponse && parsedResponse.error) {
1584
+ const upstreamError = parsedResponse.error
1585
+ const upstreamMessage = upstreamErrorMessage(upstreamError)
1586
+ throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, { upstreamError })
1587
+ }
1588
+ return { status: response.status, data: parsedResponse }
1589
+ }
1590
+
1591
+ function extractCreatedTableID(value: unknown): number {
1592
+ const candidates = [
1593
+ value,
1594
+ isRecord(value) ? value.data : undefined,
1595
+ isRecord(value) && isRecord(value.data) ? value.data.data : undefined,
1596
+ ]
1597
+ for (const candidate of candidates) {
1598
+ if (isRecord(candidate) && typeof candidate.id === 'number') {
1599
+ return candidate.id
1600
+ }
1601
+ }
1602
+ throw new CLIError('UPSTREAM_RESPONSE_INVALID', 'table create response did not include numeric table id', 1)
1603
+ }
1604
+
1605
+ function normalizeTableCreateSchemaBody(body: Record<string, any>, appId: string, tableId: number): Record<string, any> {
1606
+ const { parent: _parent, ...schemaBody } = body
1607
+ return {
1608
+ ...schemaBody,
1609
+ id: tableId,
1610
+ app_id: Number.isFinite(Number(appId)) ? Number(appId) : body.app_id,
1611
+ schema_version: typeof body.schema_version === 'number' ? body.schema_version : 0,
1612
+ layout: Array.isArray(body.layout) ? body.layout : [],
1613
+ options: isRecord(body.options)
1614
+ ? body.options
1615
+ : { Buttons: [], CustomActions: [], FrontendEvents: [], TitleFormat: { Fields: [], Parts: [], Body: '' }, misc: {} },
1616
+ workflow_enabled: typeof body.workflow_enabled === 'boolean' ? body.workflow_enabled : false,
1617
+ }
1618
+ }
1619
+
1620
+ async function executeTableCreateWithSchema(
1621
+ command: NonNullable<ReturnType<typeof findCommand>>,
1622
+ runtimeEnv: RuntimeEnv,
1623
+ flags: Record<string, string | boolean>,
1624
+ body: unknown,
1625
+ fetchImpl: typeof fetch,
1626
+ ): Promise<any> {
1627
+ const appId = flagValue(flags, 'app-id')
1628
+ if (!appId) {
1629
+ throw new CLIError('MISSING_PATH_FLAG', 'missing required flag --app-id', 2)
1630
+ }
1631
+ if (!isRecord(body)) {
1632
+ throw new CLIError('BODY_VALIDATION_FAILED', 'table create body must be an object', 2)
1633
+ }
1634
+ const createBody = {
1635
+ name: typeof body.name === 'string' ? body.name : undefined,
1636
+ parent: typeof body.parent === 'number' ? body.parent : 0,
1637
+ }
1638
+ if (!createBody.name) {
1639
+ throw new CLIError('BODY_VALIDATION_FAILED', 'table create requires body.name', 2)
1640
+ }
1641
+ const createResult = await requestJSON(runtimeEnv, command.method, resolveEndpoint(command, flags), createBody, fetchImpl)
1642
+ const tableId = extractCreatedTableID(createResult.data)
1643
+ const saveBody = normalizeTableCreateSchemaBody(body, appId, tableId)
1644
+ const saveSchema = getBodySchema('EntitySaveReqVO')
1645
+ if (!saveSchema) {
1646
+ throw new CLIError('BODY_VALIDATION_FAILED', 'EntitySaveReqVO schema is not available', 2)
1647
+ }
1648
+ const parsedSaveBody = saveSchema.safeParse(saveBody)
1649
+ if (!parsedSaveBody.success) {
1650
+ const message = parsedSaveBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
1651
+ throw new CLIError(
1652
+ 'BODY_VALIDATION_FAILED',
1653
+ message,
1654
+ 2,
1655
+ buildBodyValidationDetails(
1656
+ 'admin',
1657
+ command,
1658
+ 'EntitySaveReqVO',
1659
+ parsedSaveBody.error.issues.map((issue) => ({
1660
+ path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
1661
+ message: issue.message,
1662
+ })),
1663
+ runtimeEnv,
1664
+ ),
1665
+ )
1666
+ }
1667
+ const saveResult = await requestJSON(
1668
+ runtimeEnv,
1669
+ 'PUT',
1670
+ `/api/apps/${encodeURIComponent(appId)}/entity/${encodeURIComponent(String(tableId))}`,
1671
+ parsedSaveBody.data,
1672
+ fetchImpl,
1673
+ )
1674
+ return {
1675
+ kind: 'result',
1676
+ commandPath: command.commandPath,
1677
+ status: saveResult.status,
1678
+ data: {
1679
+ id: tableId,
1680
+ created: createResult.data,
1681
+ schema: saveResult.data,
1682
+ },
1683
+ }
1684
+ }
1685
+
565
1686
  export function summarizeCommand(scope: CommandScope, moduleName: string, commandName: string): CommandExecutionSummary {
566
1687
  const command = findCommand(scope, moduleName, commandName)
567
1688
  if (!command) {
@@ -574,37 +1695,13 @@ export function summarizeCommand(scope: CommandScope, moduleName: string, comman
574
1695
  endpoint: command.endpoint,
575
1696
  requestType: command.requestType,
576
1697
  pathFlags: command.pathParams.filter((item) => !getFixedValue(item)).map((item) => item.flag),
577
- queryFlags: command.queryParams.filter((item) => !getFixedValue(item)).map((item) => item.flag),
1698
+ queryFlags: visibleQueryParams(command).map((item) => item.flag),
578
1699
  }
579
1700
  }
580
1701
 
581
1702
  export async function executeCLI(scope: CommandScope, argv: string[], env?: RuntimeEnv, fetchImpl: typeof fetch = fetch): Promise<any> {
582
1703
  const parsed = parseCLIArgs(argv)
583
- if (scope === 'user' && parsed.commandTokens[0] === 'upload') {
584
- if (hasFlag(parsed.flags, 'help') || parsed.commandTokens.length === 1) {
585
- return { kind: 'help', text: renderUploadHelp() }
586
- }
587
- const sourcePath = parsed.commandTokens[1]
588
- if (!sourcePath) {
589
- throw new CLIError('UPLOAD_SOURCE_REQUIRED', 'upload requires a local file path', 2)
590
- }
591
- const runtimeEnv = env ?? loadRuntimeEnv(scope)
592
- const data = await uploadLocalFile(
593
- runtimeEnv,
594
- sourcePath,
595
- {
596
- filename: flagValue(parsed.flags, 'filename'),
597
- global: hasFlag(parsed.flags, 'global'),
598
- },
599
- fetchImpl,
600
- )
601
- return {
602
- kind: 'result',
603
- commandPath: ['upload', sourcePath],
604
- status: 200,
605
- data,
606
- }
607
- }
1704
+ const envInput = env ?? process.env
608
1705
  if (parsed.commandTokens.length === 0) {
609
1706
  return { kind: 'help', text: renderRootHelp(scope) }
610
1707
  }
@@ -614,24 +1711,38 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
614
1711
  }
615
1712
  const singleTokenCommand = !commandName ? findCommand(scope, moduleName) : null
616
1713
  if (!commandName && !singleTokenCommand) {
617
- return { kind: 'help', text: renderModuleHelp(scope, moduleName) }
1714
+ return { kind: 'help', text: renderModuleHelp(scope, moduleName, envInput) }
618
1715
  }
619
1716
  if (hasFlag(parsed.flags, 'help')) {
620
- return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName) }
1717
+ return { kind: 'help', text: renderCommandHelp(scope, moduleName, commandName, envInput) }
621
1718
  }
622
1719
 
623
- const runtimeEnv = env ?? loadRuntimeEnv(scope)
624
-
625
1720
  const command = findCommand(scope, moduleName, commandName)
626
1721
  if (!command) {
627
1722
  throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName}${commandName ? ` ${commandName}` : ''}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName ?? ''))
628
1723
  }
1724
+ validateKnownFlags(command, parsed.flags, envInput)
1725
+
1726
+ const runtimeEnv = env ?? loadRuntimeEnv(scope)
629
1727
 
630
1728
  const endpoint = resolveEndpoint(command, parsed.flags)
631
1729
  const scalarQuery = buildScalarQuery(command, parsed.flags)
632
1730
  const fileQuery = loadQueryFromFlags(parsed.flags)
633
- const query = { ...(scalarQuery ?? {}), ...(fileQuery ?? {}) }
634
- const body = loadBodyFromFlags(parsed.flags)
1731
+ let query = { ...(fileQuery ?? {}), ...(scalarQuery ?? {}) }
1732
+ let body: unknown
1733
+ try {
1734
+ body = loadBodyFromFlags(parsed.flags)
1735
+ } catch (error) {
1736
+ if (error instanceof CLIError && error.code === 'INVALID_BODY_JSON' && command.requestType) {
1737
+ throw new CLIError(
1738
+ 'INVALID_BODY_JSON',
1739
+ `body json is invalid for ${scope}.${command.commandPath.join(' ')}; retry with retryToolCalls[0] or retryToolCalls[1] when present`,
1740
+ 2,
1741
+ buildBodyValidationDetails(scope, command, command.requestType, [{ path: 'body', message: 'invalid JSON syntax' }], runtimeEnv),
1742
+ )
1743
+ }
1744
+ throw error
1745
+ }
635
1746
  let validatedBody = body
636
1747
 
637
1748
  if (!command.requestType && body !== undefined) {
@@ -640,9 +1751,38 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
640
1751
 
641
1752
  if (command.requestType) {
642
1753
  if (body === undefined) {
643
- throw new CLIError('MISSING_BODY_FILE', `command requires --body-file or --body-json for ${command.requestType}`, 2)
1754
+ throw new CLIError('MISSING_BODY_FILE', `command requires --body-json or --body-file for ${command.requestType}; prefer --body-json to avoid file creation`, 2)
644
1755
  }
645
- validateActionSpecificRawBody(scope, command, body)
1756
+ if (isTableCreateCommand(scope, command)) {
1757
+ requireTableCreateSchemaBody(scope, command, body, runtimeEnv)
1758
+ validateActionSpecificRawBody(scope, command, body, runtimeEnv)
1759
+ const { parent: _parent, ...schemaBody } = isRecord(body) ? body : {}
1760
+ const parsedBody = getBodySchema('EntitySaveReqVO')?.safeParse(schemaBody)
1761
+ if (!parsedBody?.success) {
1762
+ const message = parsedBody
1763
+ ? parsedBody.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; ')
1764
+ : 'EntitySaveReqVO schema is not available'
1765
+ throw new CLIError(
1766
+ 'BODY_VALIDATION_FAILED',
1767
+ message,
1768
+ 2,
1769
+ buildBodyValidationDetails(
1770
+ scope,
1771
+ command,
1772
+ 'EntitySaveReqVO',
1773
+ parsedBody && !parsedBody.success
1774
+ ? parsedBody.error.issues.map((issue) => ({
1775
+ path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
1776
+ message: issue.message,
1777
+ }))
1778
+ : [{ path: 'body', message }],
1779
+ runtimeEnv,
1780
+ ),
1781
+ )
1782
+ }
1783
+ return executeTableCreateWithSchema(command, runtimeEnv, parsed.flags, parsedBody.data, fetchImpl)
1784
+ }
1785
+ validateActionSpecificRawBody(scope, command, body, runtimeEnv)
646
1786
  const schema = getBodySchema(command.requestType)
647
1787
  if (schema) {
648
1788
  const parsedBody = schema.safeParse(body)
@@ -660,6 +1800,7 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
660
1800
  command.requestType,
661
1801
  hasUserScopeIDIssue ? 'body.options.user_scope' : 'body',
662
1802
  hasUserScopeIDIssue ? 'access-rule assign-users requires body.options.user_scope[].id to be numeric TenantUser.ID' : message,
1803
+ runtimeEnv,
663
1804
  )
664
1805
  )
665
1806
  }
@@ -667,20 +1808,20 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
667
1808
  'BODY_VALIDATION_FAILED',
668
1809
  message,
669
1810
  2,
670
- {
671
- operation: `${scope}.${command.module}.${command.functionName}`,
672
- requestType: command.requestType,
673
- issues: parsedBody.error.issues.map((issue) => ({
1811
+ buildBodyValidationDetails(
1812
+ scope,
1813
+ command,
1814
+ command.requestType,
1815
+ parsedBody.error.issues.map((issue) => ({
674
1816
  path: issue.path.length > 0 ? `body.${issue.path.join('.')}` : 'body',
675
1817
  message: issue.message,
676
1818
  })),
677
- tsHints: getTypeHints(command.requestType),
678
- docHints: getDocHints(command.requestType),
679
- }
1819
+ runtimeEnv,
1820
+ )
680
1821
  )
681
1822
  }
682
1823
  validatedBody = parsedBody.data
683
- validateActionSpecificBody(scope, command, parsed.flags, validatedBody)
1824
+ validateActionSpecificBody(scope, command, parsed.flags, validatedBody, runtimeEnv)
684
1825
  } else {
685
1826
  const unsupportedReason = getUnsupportedBodySchemaReason(command.requestType)
686
1827
  if (unsupportedReason) {
@@ -688,25 +1829,30 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
688
1829
  operation: `${scope}.${command.module}.${command.functionName}`,
689
1830
  requestType: command.requestType,
690
1831
  reason: unsupportedReason,
691
- tsHints: getTypeHints(command.requestType),
692
- docHints: getDocHints(command.requestType),
1832
+ tsHints: getTypeHints(command.requestType, runtimeEnv),
1833
+ docHints: getDocHints(command.requestType, runtimeEnv),
693
1834
  })
694
1835
  }
695
1836
  }
696
1837
  }
697
1838
 
1839
+ assertNoUserPolicyIDInput(command, runtimeEnv, query, validatedBody)
1840
+
698
1841
  if (scope === 'user' && (command.functionName === 'insertEntity' || command.functionName === 'updateEntityRow')) {
699
1842
  await validateFileFieldPayloads(scope, command, runtimeEnv, parsed.flags, validatedBody, fetchImpl)
700
1843
  }
1844
+ const policyInjected = await injectResolvedPolicyID(command, runtimeEnv, parsed.flags, query, validatedBody, fetchImpl)
1845
+ query = policyInjected.query
1846
+ validatedBody = policyInjected.body
701
1847
 
702
- const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
1848
+ const response = await fetchUpstream(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
703
1849
  method: command.method,
704
1850
  headers: {
705
1851
  ...buildRequestHeaders(runtimeEnv),
706
1852
  'Content-Type': 'application/json',
707
1853
  },
708
1854
  body: validatedBody === undefined || command.method === 'GET' ? undefined : JSON.stringify(validatedBody),
709
- })
1855
+ }, endpoint, command.method, fetchImpl)
710
1856
 
711
1857
  const raw = await response.text()
712
1858
  let parsedResponse: any = raw
@@ -716,22 +1862,12 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
716
1862
  // keep raw text
717
1863
  }
718
1864
  if (!response.ok) {
719
- throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1)
1865
+ throwUpstreamHTTPError(endpoint, command.method, response, parsedResponse)
720
1866
  }
721
1867
  if (parsedResponse && typeof parsedResponse === 'object' && 'error' in parsedResponse && parsedResponse.error) {
722
1868
  const upstreamError = parsedResponse.error
723
- const upstreamMessage =
724
- typeof upstreamError === 'object' && upstreamError && 'message' in upstreamError && typeof upstreamError.message === 'string'
725
- ? upstreamError.message
726
- : typeof upstreamError === 'string'
727
- ? upstreamError
728
- : JSON.stringify(upstreamError)
729
- const traceId =
730
- typeof parsedResponse.trace_id === 'string'
731
- ? parsedResponse.trace_id
732
- : typeof parsedResponse.request_id === 'string'
733
- ? parsedResponse.request_id
734
- : undefined
1869
+ const upstreamMessage = upstreamErrorMessage(upstreamError)
1870
+ const traceId = extractTraceID(parsedResponse)
735
1871
  const uploadBindFailure = upstreamMessage.includes('failed to bind upload to assets')
736
1872
  throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
737
1873
  endpoint,
@@ -741,19 +1877,21 @@ export async function executeCLI(scope: CommandScope, argv: string[], env?: Runt
741
1877
  ...(uploadBindFailure
742
1878
  ? {
743
1879
  reason: 'the row payload used a file/image upload entry, but Arcubase could not bind that upload into an asset record',
744
- hint: 'verify the upload storage host is reachable from the Arcubase server, then rerun arcubase upload and retry the row insert/update',
1880
+ hint: 'verify the upload storage host is reachable from the Arcubase server, then use the upload token flow again and retry the row insert/update',
745
1881
  docHints: [
746
- { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
747
- { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
1882
+ { title: 'Uploads', file: 'docs/runtime-reference/uploads.md' },
1883
+ { title: 'Row CRUD', file: 'docs/runtime-reference/row-crud.md' },
748
1884
  ],
749
1885
  }
750
1886
  : {}),
751
1887
  })
752
1888
  }
1889
+ const rowValueEvidence = buildRowValueEvidence(command, parsedResponse)
753
1890
  return {
754
1891
  kind: 'result',
755
1892
  commandPath: command.commandPath,
756
1893
  status: response.status,
757
1894
  data: parsedResponse,
1895
+ ...(rowValueEvidence ? { rowValueEvidence } : {}),
758
1896
  }
759
1897
  }