@carthooks/arcubase-cli 0.1.3 → 0.1.5

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 (64) hide show
  1. package/bundle/arcubase-admin.mjs +546 -243
  2. package/bundle/arcubase.mjs +546 -243
  3. package/dist/bin/arcubase-admin.js +0 -0
  4. package/dist/bin/arcubase.js +0 -0
  5. package/dist/generated/command_registry.generated.d.ts +36 -112
  6. package/dist/generated/command_registry.generated.d.ts.map +1 -1
  7. package/dist/generated/command_registry.generated.js +40 -163
  8. package/dist/generated/type_index.generated.d.ts +0 -12
  9. package/dist/generated/type_index.generated.d.ts.map +1 -1
  10. package/dist/generated/type_index.generated.js +0 -12
  11. package/dist/generated/zod_registry.generated.d.ts +1 -19
  12. package/dist/generated/zod_registry.generated.d.ts.map +1 -1
  13. package/dist/generated/zod_registry.generated.js +0 -21
  14. package/dist/runtime/command_registry.d.ts +19 -1
  15. package/dist/runtime/command_registry.d.ts.map +1 -1
  16. package/dist/runtime/command_registry.js +19 -1
  17. package/dist/runtime/errors.d.ts +1 -0
  18. package/dist/runtime/errors.d.ts.map +1 -1
  19. package/dist/runtime/execute.d.ts.map +1 -1
  20. package/dist/runtime/execute.js +321 -8
  21. package/dist/runtime/upload.d.ts +13 -0
  22. package/dist/runtime/upload.d.ts.map +1 -0
  23. package/dist/runtime/upload.js +148 -0
  24. package/dist/runtime/zod_registry.d.ts.map +1 -1
  25. package/dist/runtime/zod_registry.js +50 -30
  26. package/dist/tests/command_registry.test.js +11 -4
  27. package/dist/tests/execute_validation.test.js +221 -13
  28. package/dist/tests/help.test.js +29 -6
  29. package/dist/tests/upload.test.d.ts +2 -0
  30. package/dist/tests/upload.test.d.ts.map +1 -0
  31. package/dist/tests/upload.test.js +107 -0
  32. package/package.json +1 -1
  33. package/sdk-dist/api/admin/app.ts +8 -1
  34. package/sdk-dist/docs/runtime-reference/README.md +33 -15
  35. package/sdk-dist/docs/runtime-reference/app-discovery.md +68 -0
  36. package/sdk-dist/docs/runtime-reference/entity-schema/README.md +2 -2
  37. package/sdk-dist/docs/runtime-reference/entity-schema/file.md +9 -0
  38. package/sdk-dist/docs/runtime-reference/entity-schema/image.md +9 -0
  39. package/sdk-dist/docs/runtime-reference/entity-schema/subform.md +2 -2
  40. package/sdk-dist/docs/runtime-reference/entity-schema.md +2 -2
  41. package/sdk-dist/docs/runtime-reference/examples/README.md +1 -1
  42. package/sdk-dist/docs/runtime-reference/examples/oms-01/README.md +23 -4
  43. package/sdk-dist/docs/runtime-reference/examples/oms-01/app-overview.md +1 -1
  44. package/sdk-dist/docs/runtime-reference/row-crud.md +8 -6
  45. package/sdk-dist/docs/runtime-reference/search-and-bulk-actions.md +5 -5
  46. package/sdk-dist/docs/runtime-reference/table-lifecycle.md +34 -6
  47. package/sdk-dist/docs/runtime-reference/uploads.md +106 -0
  48. package/sdk-dist/generated/command_registry.generated.ts +40 -163
  49. package/sdk-dist/generated/type_index.generated.ts +0 -12
  50. package/sdk-dist/generated/zod_registry.generated.ts +0 -36
  51. package/sdk-dist/types/app.ts +7 -0
  52. package/src/generated/command_registry.generated.ts +40 -163
  53. package/src/generated/type_index.generated.ts +0 -12
  54. package/src/generated/zod_registry.generated.ts +0 -36
  55. package/src/runtime/command_registry.ts +38 -2
  56. package/src/runtime/errors.ts +1 -0
  57. package/src/runtime/execute.ts +368 -8
  58. package/src/runtime/upload.ts +196 -0
  59. package/src/runtime/zod_registry.ts +50 -30
  60. package/src/tests/command_registry.test.ts +13 -4
  61. package/src/tests/execute_validation.test.ts +257 -13
  62. package/src/tests/help.test.ts +35 -6
  63. package/src/tests/upload.test.ts +133 -0
  64. package/sdk-dist/docs/runtime-reference/workflow/README.md +0 -19
@@ -5,13 +5,122 @@ import { buildRequestHeaders, buildURL, normalizeExternalEndpoint } from './http
5
5
  import { listModules, listModuleCommands, findCommand, findCommandSuggestions } 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';
9
+ function summarizeUploadResult(value) {
10
+ return value;
11
+ }
12
+ function parseJSONResponse(raw) {
13
+ if (!raw)
14
+ return null;
15
+ try {
16
+ return JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return raw;
20
+ }
21
+ }
22
+ function unwrapResponseData(payload) {
23
+ return isRecord(payload) && 'data' in payload ? payload.data : payload;
24
+ }
25
+ function formatUpstreamError(payload) {
26
+ if (typeof payload === 'string') {
27
+ return payload;
28
+ }
29
+ if (isRecord(payload) && isRecord(payload.error) && typeof payload.error.message === 'string') {
30
+ return payload.error.message;
31
+ }
32
+ return JSON.stringify(payload);
33
+ }
34
+ function extractApps(payload) {
35
+ const data = unwrapResponseData(payload);
36
+ if (!Array.isArray(data))
37
+ return [];
38
+ return data
39
+ .filter((item) => isRecord(item))
40
+ .filter((item) => item.id !== undefined && typeof item.name === 'string')
41
+ .map((item) => ({
42
+ id: String(item.id),
43
+ name: item.name,
44
+ }));
45
+ }
46
+ function extractEntities(payload) {
47
+ const data = unwrapResponseData(payload);
48
+ if (!Array.isArray(data))
49
+ return [];
50
+ return data
51
+ .filter((item) => isRecord(item))
52
+ .filter((item) => item.id !== undefined && typeof item.name === 'string')
53
+ .map((item) => ({
54
+ id: String(item.id),
55
+ name: item.name,
56
+ }));
57
+ }
58
+ async function executeAppInventory(runtimeEnv, fetchImpl) {
59
+ const appsResponse = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, '/apps'), {
60
+ method: 'GET',
61
+ headers: buildRequestHeaders(runtimeEnv),
62
+ });
63
+ const appsRaw = await appsResponse.text();
64
+ const appsPayload = parseJSONResponse(appsRaw);
65
+ if (!appsResponse.ok) {
66
+ throw new CLIError('UPSTREAM_REQUEST_FAILED', formatUpstreamError(appsPayload), 1);
67
+ }
68
+ if (isRecord(appsPayload) && appsPayload.error) {
69
+ throw new CLIError('UPSTREAM_BODY_ERROR', formatUpstreamError(appsPayload), 1, {
70
+ endpoint: '/apps',
71
+ method: 'GET',
72
+ upstreamError: appsPayload.error,
73
+ });
74
+ }
75
+ const apps = extractApps(appsPayload);
76
+ const items = [];
77
+ for (const app of apps) {
78
+ const entitiesResponse = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/apps/${encodeURIComponent(app.id)}/entities`), {
79
+ method: 'POST',
80
+ headers: {
81
+ ...buildRequestHeaders(runtimeEnv),
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ body: JSON.stringify({}),
85
+ });
86
+ const entitiesRaw = await entitiesResponse.text();
87
+ const entitiesPayload = parseJSONResponse(entitiesRaw);
88
+ if (!entitiesResponse.ok) {
89
+ throw new CLIError('UPSTREAM_REQUEST_FAILED', formatUpstreamError(entitiesPayload), 1);
90
+ }
91
+ if (isRecord(entitiesPayload) && entitiesPayload.error) {
92
+ throw new CLIError('UPSTREAM_BODY_ERROR', formatUpstreamError(entitiesPayload), 1, {
93
+ endpoint: `/apps/${app.id}/entities`,
94
+ method: 'POST',
95
+ upstreamError: entitiesPayload.error,
96
+ });
97
+ }
98
+ items.push({
99
+ ...app,
100
+ entities: extractEntities(entitiesPayload),
101
+ });
102
+ }
103
+ return {
104
+ kind: 'result',
105
+ commandPath: ['app', 'inventory'],
106
+ status: 200,
107
+ data: {
108
+ apps: items,
109
+ },
110
+ };
111
+ }
8
112
  export function renderRootHelp(scope) {
9
113
  const binary = scope === 'admin' ? 'arcubase-admin' : 'arcubase';
114
+ const items = listModules(scope).map((item) => ` - ${item}`);
10
115
  return [
11
116
  `${binary} <module> <command> [flags]`,
12
117
  '',
118
+ ...(scope === 'admin'
119
+ ? ['use arcubase-admin for app, entity shell, and schema management', '']
120
+ : ['use arcubase for rows, search, selection actions, and bulk actions', '']),
13
121
  'modules:',
14
- ...listModules(scope).map((item) => ` - ${item}`),
122
+ ...items,
123
+ ...(scope === 'user' ? ['', 'special commands:', ' - upload <local-file>'] : []),
15
124
  ].join('\n');
16
125
  }
17
126
  export function renderModuleHelp(scope, moduleName) {
@@ -23,15 +132,28 @@ export function renderModuleHelp(scope, moduleName) {
23
132
  return [
24
133
  `${scope === 'admin' ? 'arcubase-admin' : 'arcubase'} ${moduleName} <command> [flags]`,
25
134
  '',
135
+ ...(scope === 'admin'
136
+ ? ['use arcubase-admin for app, entity shell, and schema management', '']
137
+ : ['use arcubase for rows, search, selection actions, and bulk actions', '']),
26
138
  'commands:',
27
139
  ...items.map((item) => ` - ${item.commandPath[1]}`),
28
140
  ].join('\n');
29
141
  }
142
+ function buildUnknownCommandDetails(scope, moduleName, commandName) {
143
+ const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`);
144
+ if (scope === 'admin' && moduleName === 'workflow' && suggestions.some((item) => item.startsWith('arcubase rows '))) {
145
+ return {
146
+ suggestions,
147
+ reason: `${moduleName} ${commandName} is not a valid command group; use arcubase rows for row operations`,
148
+ hint: `use: arcubase rows ${commandName}`,
149
+ };
150
+ }
151
+ return suggestions.length > 0 ? { suggestions } : undefined;
152
+ }
30
153
  export function renderCommandHelp(scope, moduleName, commandName) {
31
154
  const command = findCommand(scope, moduleName, commandName);
32
155
  if (!command) {
33
- const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`);
34
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, suggestions.length > 0 ? { suggestions } : undefined);
156
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName));
35
157
  }
36
158
  const docHints = [
37
159
  ...getCommandDocHints(`${scope}.${command.module}.${command.functionName}`),
@@ -45,6 +167,21 @@ export function renderCommandHelp(scope, moduleName, commandName) {
45
167
  'query flags: --query-file',
46
168
  command.endpointParams.length ? `path flags: ${command.endpointParams.map((item) => `--${item.replace(/_/g, '-')}`).join(', ')}` : 'path flags: none',
47
169
  command.scalarParams.length ? `scalar flags: ${command.scalarParams.map((item) => `--${item.name.replace(/([A-Z])/g, '-$1').toLowerCase()}`).join(', ')}` : 'scalar flags: none',
170
+ ...(scope === 'admin' && moduleName === 'entity' && commandName === 'admin-save-entity'
171
+ ? [
172
+ 'next step:',
173
+ ' - switch to arcubase for row operations',
174
+ ' - row insert: arcubase rows insert-entity --app-id <app_id> --entity-id <entity_id> --body-file insert.json',
175
+ ' - row query: arcubase rows query-entity --app-id <app_id> --entity-id <entity_id> --body-file query.json',
176
+ ]
177
+ : []),
178
+ ...(scope === 'admin' && moduleName === 'app' && commandName === 'inventory'
179
+ ? [
180
+ 'purpose:',
181
+ ' - list all accessible apps and each app entity list in one call sequence',
182
+ ' - use this before asking the user for app_id',
183
+ ]
184
+ : []),
48
185
  ...(docHints.length > 0
49
186
  ? ['docs:', ...docHints.map((item) => ` - ${item.title}: ${item.file}`)]
50
187
  : []),
@@ -97,6 +234,146 @@ function buildScalarQuery(command, flags) {
97
234
  function isRecord(value) {
98
235
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
99
236
  }
237
+ function extractEntityFields(payload) {
238
+ const candidate = isRecord(payload) && isRecord(payload.data) ? payload.data : payload;
239
+ if (!isRecord(candidate) || !Array.isArray(candidate.fields)) {
240
+ return [];
241
+ }
242
+ return candidate.fields
243
+ .filter((item) => isRecord(item))
244
+ .filter((item) => typeof item.id === 'number' && typeof item.type === 'string')
245
+ .map((item) => ({
246
+ id: item.id,
247
+ type: item.type,
248
+ label: typeof item.label === 'string' ? item.label : undefined,
249
+ }));
250
+ }
251
+ async function fetchEntityFields(runtimeEnv, appId, entityId, fetchImpl) {
252
+ const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, `/entity/${encodeURIComponent(appId)}/${encodeURIComponent(entityId)}`), {
253
+ method: 'GET',
254
+ headers: buildRequestHeaders(runtimeEnv),
255
+ });
256
+ const raw = await response.text();
257
+ let parsedResponse = raw;
258
+ try {
259
+ parsedResponse = raw ? JSON.parse(raw) : null;
260
+ }
261
+ catch {
262
+ parsedResponse = raw;
263
+ }
264
+ if (!response.ok) {
265
+ throw new CLIError('UPSTREAM_REQUEST_FAILED', typeof parsedResponse === 'string' ? parsedResponse : JSON.stringify(parsedResponse), 1);
266
+ }
267
+ if (isRecord(parsedResponse) && parsedResponse.error) {
268
+ const upstreamError = parsedResponse.error;
269
+ const upstreamMessage = isRecord(upstreamError) && typeof upstreamError.message === 'string'
270
+ ? upstreamError.message
271
+ : typeof upstreamError === 'string'
272
+ ? upstreamError
273
+ : JSON.stringify(upstreamError);
274
+ throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
275
+ endpoint: `/entity/${appId}/${entityId}`,
276
+ method: 'GET',
277
+ upstreamError,
278
+ });
279
+ }
280
+ const fields = extractEntityFields(parsedResponse);
281
+ return new Map(fields.map((field) => [field.id, field]));
282
+ }
283
+ function looksLikeLocalFilePath(value) {
284
+ return (value.startsWith('./') ||
285
+ value.startsWith('../') ||
286
+ value.startsWith('/') ||
287
+ value.startsWith('~/') ||
288
+ /\.(pdf|png|jpe?g|gif|webp|bmp|svg|heic|txt|csv|xlsx?|docx?|pptx?|zip)$/i.test(value));
289
+ }
290
+ function isValidUploadEntry(value) {
291
+ if (!isRecord(value)) {
292
+ return false;
293
+ }
294
+ const hasUploadId = typeof value.upload_id === 'number' || typeof value.upload_id === 'string';
295
+ const hasAssetsId = typeof value.assets_id === 'number' || typeof value.assets_id === 'string';
296
+ return (hasUploadId || hasAssetsId) && typeof value.file === 'string' && typeof value.file_name === 'string';
297
+ }
298
+ async function validateFileFieldPayloads(scope, command, runtimeEnv, flags, body, fetchImpl) {
299
+ if (!isRecord(body)) {
300
+ return;
301
+ }
302
+ const container = command.functionName === 'updateEntityRow' ? body.data : body.fields;
303
+ if (!isRecord(container)) {
304
+ return;
305
+ }
306
+ const appId = flagValue(flags, 'app-id');
307
+ const entityId = flagValue(flags, 'entity-id');
308
+ if (!appId || !entityId) {
309
+ return;
310
+ }
311
+ const fieldsById = await fetchEntityFields(runtimeEnv, appId, entityId, fetchImpl);
312
+ for (const [fieldIdText, value] of Object.entries(container)) {
313
+ if (!/^\d+$/.test(fieldIdText)) {
314
+ continue;
315
+ }
316
+ const fieldId = Number(fieldIdText);
317
+ const field = fieldsById.get(fieldId);
318
+ if (!field || (field.type !== 'file' && field.type !== 'image')) {
319
+ continue;
320
+ }
321
+ const fieldPath = command.functionName === 'updateEntityRow' ? `body.data.${fieldId}` : `body.fields.${fieldId}`;
322
+ const fieldName = field.label ? `${field.label} (${field.type})` : `${field.type} field ${fieldId}`;
323
+ if (typeof value === 'string') {
324
+ const guidance = looksLikeLocalFilePath(value)
325
+ ? `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`
326
+ : `${fieldName} must be an array returned by "arcubase upload <local-file>", not a string`;
327
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
328
+ operation: `${scope}.${command.module}.${command.functionName}`,
329
+ requestType: command.requestType ?? undefined,
330
+ issues: [{ path: fieldPath, message: guidance }],
331
+ 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' },
334
+ ],
335
+ });
336
+ }
337
+ if (isRecord(value)) {
338
+ const guidance = `${fieldName} must be an array. Run "arcubase upload <local-file>" and use the returned data array directly as the field value`;
339
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
340
+ operation: `${scope}.${command.module}.${command.functionName}`,
341
+ requestType: command.requestType ?? undefined,
342
+ issues: [{ path: fieldPath, message: guidance }],
343
+ docHints: [
344
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
345
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
346
+ ],
347
+ });
348
+ }
349
+ if (!Array.isArray(value)) {
350
+ const guidance = `${fieldName} must be an array returned by "arcubase upload <local-file>"`;
351
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
352
+ operation: `${scope}.${command.module}.${command.functionName}`,
353
+ requestType: command.requestType ?? undefined,
354
+ issues: [{ path: fieldPath, message: guidance }],
355
+ docHints: [
356
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
357
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
358
+ ],
359
+ });
360
+ }
361
+ for (const [index, item] of value.entries()) {
362
+ if (!isValidUploadEntry(item)) {
363
+ 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`;
364
+ throw new CLIError('BODY_VALIDATION_FAILED', guidance, 2, {
365
+ operation: `${scope}.${command.module}.${command.functionName}`,
366
+ requestType: command.requestType ?? undefined,
367
+ issues: [{ path: `${fieldPath}.${index}`, message: guidance }],
368
+ docHints: [
369
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
370
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
371
+ ],
372
+ });
373
+ }
374
+ }
375
+ }
376
+ }
100
377
  function throwBodyValidationFailure(message, requestType, path, scope, command) {
101
378
  throw new CLIError('BODY_VALIDATION_FAILED', message, 2, {
102
379
  operation: `${scope}.${command.module}.${command.functionName}`,
@@ -115,7 +392,7 @@ function validateActionSpecificBody(scope, command, flags, body) {
115
392
  if (!isRecord(body) || !command.requestType) {
116
393
  return;
117
394
  }
118
- if (command.module === 'workflow' && command.functionName === 'queryEntitySelection') {
395
+ if (command.module === 'rows' && command.functionName === 'queryEntitySelection') {
119
396
  const action = flagValue(flags, 'action');
120
397
  const selection = body.selection;
121
398
  if (!isRecord(selection) || typeof selection.type !== 'string') {
@@ -126,7 +403,7 @@ function validateActionSpecificBody(scope, command, flags, body) {
126
403
  const conditionKeys = condition ? Object.keys(condition).filter((key) => key !== 'selectAll') : [];
127
404
  if (action === 'query') {
128
405
  if (type === 'condition') {
129
- throwBodyValidationFailure('selection.type=condition is not supported for action=query; use selection.type=ids or selection.type=all, or use workflow query-entity for search conditions', command.requestType, 'body.selection.type', scope, command);
406
+ throwBodyValidationFailure('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', command.requestType, 'body.selection.type', scope, command);
130
407
  }
131
408
  return;
132
409
  }
@@ -141,8 +418,7 @@ function validateActionSpecificBody(scope, command, flags, body) {
141
418
  export function summarizeCommand(scope, moduleName, commandName) {
142
419
  const command = findCommand(scope, moduleName, commandName);
143
420
  if (!command) {
144
- const suggestions = findCommandSuggestions(moduleName, commandName).map((item) => `${item.binary} ${item.moduleName} ${item.commandName}`);
145
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, suggestions.length > 0 ? { suggestions } : undefined);
421
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName));
146
422
  }
147
423
  return {
148
424
  scope,
@@ -156,6 +432,26 @@ export function summarizeCommand(scope, moduleName, commandName) {
156
432
  }
157
433
  export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
158
434
  const parsed = parseCLIArgs(argv);
435
+ if (scope === 'user' && parsed.commandTokens[0] === 'upload') {
436
+ if (hasFlag(parsed.flags, 'help') || parsed.commandTokens.length === 1) {
437
+ return { kind: 'help', text: renderUploadHelp() };
438
+ }
439
+ const sourcePath = parsed.commandTokens[1];
440
+ if (!sourcePath) {
441
+ throw new CLIError('UPLOAD_SOURCE_REQUIRED', 'upload requires a local file path', 2);
442
+ }
443
+ const runtimeEnv = env ?? loadRuntimeEnv(scope);
444
+ const data = await uploadLocalFile(runtimeEnv, sourcePath, {
445
+ filename: flagValue(parsed.flags, 'filename'),
446
+ global: hasFlag(parsed.flags, 'global'),
447
+ }, fetchImpl);
448
+ return {
449
+ kind: 'result',
450
+ commandPath: ['upload', sourcePath],
451
+ status: 200,
452
+ data,
453
+ };
454
+ }
159
455
  if (parsed.commandTokens.length === 0) {
160
456
  return { kind: 'help', text: renderRootHelp(scope) };
161
457
  }
@@ -172,7 +468,10 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
172
468
  const runtimeEnv = env ?? loadRuntimeEnv(scope);
173
469
  const command = findCommand(scope, moduleName, commandName);
174
470
  if (!command) {
175
- throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2);
471
+ throw new CLIError('UNKNOWN_COMMAND', `unknown command: ${moduleName} ${commandName}`, 2, buildUnknownCommandDetails(scope, moduleName, commandName));
472
+ }
473
+ if (scope === 'admin' && moduleName === 'app' && commandName === 'inventory') {
474
+ return executeAppInventory(runtimeEnv, fetchImpl);
176
475
  }
177
476
  const endpoint = normalizeExternalEndpoint(resolveEndpoint(command.endpoint, parsed.flags));
178
477
  const scalarQuery = buildScalarQuery(command, parsed.flags);
@@ -215,6 +514,9 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
215
514
  }
216
515
  }
217
516
  }
517
+ if (scope === 'user' && (command.functionName === 'insertEntity' || command.functionName === 'updateEntityRow')) {
518
+ await validateFileFieldPayloads(scope, command, runtimeEnv, parsed.flags, validatedBody, fetchImpl);
519
+ }
218
520
  const response = await fetchImpl(buildURL(runtimeEnv.ARCUBASE_BASE_URL, endpoint, Object.keys(query).length > 0 ? query : undefined), {
219
521
  method: command.method,
220
522
  headers: {
@@ -246,11 +548,22 @@ export async function executeCLI(scope, argv, env, fetchImpl = fetch) {
246
548
  : typeof parsedResponse.request_id === 'string'
247
549
  ? parsedResponse.request_id
248
550
  : undefined;
551
+ const uploadBindFailure = upstreamMessage.includes('failed to bind upload to assets');
249
552
  throw new CLIError('UPSTREAM_BODY_ERROR', upstreamMessage, 1, {
250
553
  endpoint,
251
554
  method: command.method,
252
555
  traceId,
253
556
  upstreamError,
557
+ ...(uploadBindFailure
558
+ ? {
559
+ reason: 'the row payload used a file/image upload entry, but Arcubase could not bind that upload into an asset record',
560
+ hint: 'verify the upload storage host is reachable from the Arcubase server, then rerun arcubase upload and retry the row insert/update',
561
+ docHints: [
562
+ { title: 'Uploads', file: '/opt/arcubase-sdk/docs/runtime-reference/uploads.md' },
563
+ { title: 'Row CRUD', file: '/opt/arcubase-sdk/docs/runtime-reference/row-crud.md' },
564
+ ],
565
+ }
566
+ : {}),
254
567
  });
255
568
  }
256
569
  return {
@@ -0,0 +1,13 @@
1
+ import type { RuntimeEnv } from './env.js';
2
+ export type ArcubaseUploadResult = {
3
+ upload_id: number | string;
4
+ file: string;
5
+ file_name: string;
6
+ meta: Record<string, never>;
7
+ };
8
+ export declare function renderUploadHelp(): string;
9
+ export declare function uploadLocalFile(env: RuntimeEnv, sourcePath: string, options: {
10
+ filename?: string;
11
+ global?: boolean;
12
+ }, fetchImpl?: typeof fetch): Promise<ArcubaseUploadResult[]>;
13
+ //# sourceMappingURL=upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/runtime/upload.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAuB1C,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;CAC5B,CAAA;AA+CD,wBAAgB,gBAAgB,IAAI,MAAM,CA0BzC;AAED,wBAAsB,eAAe,CACnC,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,EACD,SAAS,GAAE,OAAO,KAAa,GAC9B,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAiFjC"}
@@ -0,0 +1,148 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { CLIError } from './errors.js';
4
+ import { buildRequestHeaders, buildURL } from './http.js';
5
+ function readUploadSource(sourcePath) {
6
+ const absolutePath = path.resolve(sourcePath);
7
+ if (!fs.existsSync(absolutePath)) {
8
+ throw new CLIError('UPLOAD_SOURCE_NOT_FOUND', `upload source file does not exist: ${sourcePath}`, 2);
9
+ }
10
+ const stat = fs.statSync(absolutePath);
11
+ if (!stat.isFile()) {
12
+ throw new CLIError('UPLOAD_SOURCE_INVALID', `upload source path must be a file: ${sourcePath}`, 2);
13
+ }
14
+ return {
15
+ absolutePath,
16
+ bytes: fs.readFileSync(absolutePath),
17
+ };
18
+ }
19
+ function parseJSONResponse(raw) {
20
+ try {
21
+ return raw ? JSON.parse(raw) : null;
22
+ }
23
+ catch {
24
+ return raw;
25
+ }
26
+ }
27
+ function requireTokenPayload(payload) {
28
+ if (payload && typeof payload === 'object' && payload.error) {
29
+ const upstreamError = payload.error;
30
+ const message = typeof upstreamError === 'object' && upstreamError && 'message' in upstreamError && typeof upstreamError.message === 'string'
31
+ ? upstreamError.message
32
+ : typeof upstreamError === 'string'
33
+ ? upstreamError
34
+ : JSON.stringify(upstreamError);
35
+ throw new CLIError('UPSTREAM_BODY_ERROR', message, 1, {
36
+ endpoint: '/upload/token',
37
+ method: 'POST',
38
+ traceId: typeof payload.trace_id === 'string' ? payload.trace_id : undefined,
39
+ upstreamError,
40
+ });
41
+ }
42
+ if (!payload?.data || typeof payload.data !== 'object') {
43
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response did not include data', 1);
44
+ }
45
+ return payload.data;
46
+ }
47
+ export function renderUploadHelp() {
48
+ return [
49
+ 'arcubase upload <local-file> [flags]',
50
+ '',
51
+ 'purpose:',
52
+ ' - upload one local file through Arcubase',
53
+ ' - return a field value that can be used directly in file or image row payloads',
54
+ '',
55
+ 'flags:',
56
+ ' --filename <name> override the uploaded filename stored in Arcubase',
57
+ ' --global request a global upload token instead of a tenant-scoped token',
58
+ '',
59
+ 'result shape:',
60
+ ' - data is a JSON array',
61
+ ' - use that array directly as the value of a file or image field in insert/update payloads',
62
+ '',
63
+ 'example:',
64
+ ' arcubase upload ./contract.pdf',
65
+ ' arcubase upload ./photo.jpg --filename lead-photo.jpg',
66
+ '',
67
+ 'docs:',
68
+ ' - Uploads: /opt/arcubase-sdk/docs/runtime-reference/uploads.md',
69
+ ' - Row CRUD: /opt/arcubase-sdk/docs/runtime-reference/row-crud.md',
70
+ ' - File field: /opt/arcubase-sdk/docs/runtime-reference/entity-schema/file.md',
71
+ ' - Image field: /opt/arcubase-sdk/docs/runtime-reference/entity-schema/image.md',
72
+ ].join('\n');
73
+ }
74
+ export async function uploadLocalFile(env, sourcePath, options, fetchImpl = fetch) {
75
+ const { absolutePath, bytes } = readUploadSource(sourcePath);
76
+ const filename = (options.filename && options.filename.trim()) || path.basename(absolutePath);
77
+ const tokenResponse = await fetchImpl(buildURL(env.ARCUBASE_BASE_URL, '/upload/token'), {
78
+ method: 'POST',
79
+ headers: {
80
+ ...buildRequestHeaders(env),
81
+ 'Content-Type': 'application/json',
82
+ },
83
+ body: JSON.stringify({
84
+ global: Boolean(options.global),
85
+ }),
86
+ });
87
+ const tokenRaw = await tokenResponse.text();
88
+ const tokenParsed = parseJSONResponse(tokenRaw);
89
+ if (!tokenResponse.ok) {
90
+ throw new CLIError('UPLOAD_TOKEN_REQUEST_FAILED', typeof tokenParsed === 'string' ? tokenParsed : JSON.stringify(tokenParsed), 1, {
91
+ endpoint: '/upload/token',
92
+ method: 'POST',
93
+ });
94
+ }
95
+ const tokenData = requireTokenPayload(tokenParsed);
96
+ if (tokenData.mode !== 'aliyun-oss' && tokenData.mode !== 's3-post-policy') {
97
+ throw new CLIError('UPLOAD_MODE_UNSUPPORTED', `unsupported upload mode: ${String(tokenData.mode || '')}`, 1);
98
+ }
99
+ if (!tokenData.token_data?.dir || !tokenData.token_data.policy || !tokenData.token_data.host) {
100
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required token_data fields', 1);
101
+ }
102
+ if (tokenData.id === undefined || tokenData.id === null || tokenData.id === '') {
103
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing id', 1);
104
+ }
105
+ const form = new FormData();
106
+ form.append('key', `${tokenData.token_data.dir}${filename}`);
107
+ form.append('policy', tokenData.token_data.policy);
108
+ if (tokenData.mode === 'aliyun-oss') {
109
+ if (!tokenData.token_data.accessid || !tokenData.token_data.signature) {
110
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required token_data fields', 1);
111
+ }
112
+ form.append('OSSAccessKeyId', tokenData.token_data.accessid);
113
+ form.append('success_action_status', '200');
114
+ form.append('signature', tokenData.token_data.signature);
115
+ }
116
+ else {
117
+ if (!tokenData.token_data.x_amz_algorithm ||
118
+ !tokenData.token_data.x_amz_credential ||
119
+ !tokenData.token_data.x_amz_date ||
120
+ !tokenData.token_data.x_amz_signature) {
121
+ throw new CLIError('UPLOAD_TOKEN_INVALID', 'upload token response is missing required s3 token_data fields', 1);
122
+ }
123
+ form.append('x-amz-algorithm', tokenData.token_data.x_amz_algorithm);
124
+ form.append('x-amz-credential', tokenData.token_data.x_amz_credential);
125
+ form.append('x-amz-date', tokenData.token_data.x_amz_date);
126
+ form.append('x-amz-signature', tokenData.token_data.x_amz_signature);
127
+ }
128
+ form.append('file', new Blob([new Uint8Array(bytes)]), filename);
129
+ const uploadResponse = await fetchImpl(tokenData.token_data.host, {
130
+ method: 'POST',
131
+ body: form,
132
+ });
133
+ const uploadRaw = await uploadResponse.text();
134
+ if (!uploadResponse.ok) {
135
+ throw new CLIError('UPLOAD_TRANSFER_FAILED', uploadRaw || `upload failed with status ${uploadResponse.status}`, 1, {
136
+ endpoint: tokenData.token_data.host,
137
+ method: 'POST',
138
+ });
139
+ }
140
+ return [
141
+ {
142
+ upload_id: tokenData.id,
143
+ file: filename,
144
+ file_name: filename,
145
+ meta: {},
146
+ },
147
+ ];
148
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"zod_registry.d.ts","sourceRoot":"","sources":["../../src/runtime/zod_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AA6BrC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAOjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQ9D;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AA2DD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,CAqBhE;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,CAI9D;AAsDD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAItE"}
1
+ {"version":3,"file":"zod_registry.d.ts","sourceRoot":"","sources":["../../src/runtime/zod_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,KAAK,CAAA;AA6BrC,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAOjE;AAED,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI9E;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAQ9D;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAgED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,EAAE,CAqBhE;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,EAAE,CAI9D;AAqED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAItE"}