openyida 2026.9.8 → 2026.9.10

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 (71) hide show
  1. package/README.md +4 -1
  2. package/lib/app/create-form/args.js +40 -2
  3. package/lib/app/create-form/batch.js +205 -20
  4. package/lib/app/create-form/commands.js +2 -0
  5. package/lib/app/create-form.js +337 -14
  6. package/lib/app/services/canvas-page-schema-builder.js +3 -3
  7. package/lib/core/cli-error.js +10 -1
  8. package/lib/core/command-manifest.js +5 -2
  9. package/lib/core/locales/en.js +9 -1
  10. package/lib/core/locales/zh.js +9 -1
  11. package/lib/core/sample.js +13 -1
  12. package/lib/design-plan/init.js +90 -12
  13. package/lib/design-plan/materialize.js +18 -16
  14. package/lib/design-plan/normalize.js +88 -1
  15. package/lib/process/configure-process.js +18 -1
  16. package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +107 -23
  17. package/lib/samples/openyida-scaffold/canvas-nav/content.jsx +42 -0
  18. package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
  19. package/lib/samples/yida-canvas-table-form/table-form-batch-submit.canvas.jsx +84 -17
  20. package/lib/samples/yida-rechart/trend-combo.canvas.jsx +97 -18
  21. package/package.json +1 -1
  22. package/yida-skills/skills/yida-app/SKILL.md +11 -2
  23. package/yida-skills/skills/yida-app/workflow/parallel-work.md +3 -1
  24. package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +7 -5
  25. package/yida-skills/skills/yida-app/workflow/plan/workflow.md +8 -5
  26. package/yida-skills/skills/yida-app/workflow/step-2-design.md +4 -0
  27. package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +4 -2
  28. package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +15 -3
  29. package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +4 -1
  30. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +2 -0
  31. package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +27 -7
  32. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-authoring-examples.md +2 -41
  33. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +93 -65
  34. package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +89 -0
  35. package/yida-skills/skills/yida-canvas-custom-page/references/component-library-guide.md +2 -2
  36. package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +11 -3
  37. package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +6 -2
  38. package/yida-skills/skills/yida-canvas-custom-page/scripts/build-canvas-theme.js +122 -0
  39. package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +102 -0
  40. package/yida-skills/skills/yida-canvas-table-form/SKILL.md +2 -2
  41. package/yida-skills/skills/yida-create-form-page/SKILL.md +68 -3
  42. package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +18 -2
  43. package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +18 -4
  44. package/yida-skills/skills/yida-design/references/design-mode.md +1 -1
  45. package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +2 -4
  46. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +3 -1
  47. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +2 -2
  48. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +3 -3
  49. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +3 -3
  50. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +4 -4
  51. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +3 -3
  52. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +7 -7
  53. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +2 -2
  54. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +4 -4
  55. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +3 -3
  56. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +5 -5
  57. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +2 -2
  58. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +4 -4
  59. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +2 -2
  60. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +2 -2
  61. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +4 -4
  62. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +4 -4
  63. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +5 -5
  64. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +2 -2
  65. package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +3 -3
  66. package/yida-skills/skills/yida-design/workflow/output-design.md +1 -1
  67. package/yida-skills/skills/yida-nav-shell/SKILL.md +7 -2
  68. package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +84 -3
  69. package/yida-skills/skills/yida-prd/workflow/plan-business.md +3 -1
  70. package/yida-skills/skills/yida-requirement-analysis/SKILL.md +9 -8
  71. package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +23 -1
package/README.md CHANGED
@@ -174,6 +174,8 @@ openyida update-app APP_XXX --theme-file .cache/openyida/crm/app-theme.css --nav
174
174
  openyida corp-efficiency
175
175
  openyida create-form create APP_XXX "Customer" .cache/openyida/forms/customer-fields.json
176
176
  openyida create-form update APP_XXX FORM_XXX .cache/openyida/forms/customer-changes.json
177
+ openyida create-form update APP_XXX FORM_XXX --data-file .cache/openyida/forms/customer-changes.json
178
+ openyida create-form resume APP_XXX FORM_XXX .cache/openyida/forms/customer-fields.json --json
177
179
  openyida sample openyida-page-template form-fields --output .cache/openyida/forms/customer-fields.json
178
180
  openyida sample openyida-page-template canvas-form-drawer --output project/pages/src/customer-entry.canvas.jsx --var APP_TYPE=APP_XXX --var FORM_UUID=FORM_XXX
179
181
  openyida get-schema APP_XXX FORM_XXX
@@ -413,7 +415,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
413
415
  | `openyida create-form create <appType> "<formTitle>" <fieldsJsonFile> [--icon auto\|<iconName>] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a form page |
414
416
  | `openyida create-form icons [--json]` | List available form navigation icons |
415
417
  | `openyida create-form validate-fields <fieldsJsonOrFile> [--json]` | Validate form field JSON locally |
416
- | `openyida create-form update <appType> ... [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Update a form page |
418
+ | `openyida create-form update <appType> <formUuid> (<changesJsonOrFile> \| --data-file <changesJsonOrFile>) [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Update a form page |
419
+ | `openyida create-form resume <appType> <formUuid> <fieldsJsonOrFile> [--json]` | Update a form page |
417
420
  | `openyida create-form patch <appType> <formUuid> <patchJsonOrFile> [--open\|--no-open]` | Update a form page |
418
421
  | `openyida create-form rule <appType> <formUuid> <rulesJsonOrFile> [--open\|--no-open]` | Update a form page |
419
422
  | `openyida create-form validation <appType> <formUuid> <validationsJsonOrFile> [--open\|--no-open]` | Update a form page |
@@ -89,6 +89,7 @@ function createParseArgs(dependencies) {
89
89
  contentLocale: null,
90
90
  browserOpenMode: openOption.mode,
91
91
  };
92
+ let dataFileInput;
92
93
 
93
94
  const args = [...rawArgs];
94
95
 
@@ -111,6 +112,14 @@ function createParseArgs(dependencies) {
111
112
  i--;
112
113
  } else if (args[i] === '--icon') {
113
114
  throwCreateFormError('--icon requires a value', 'CREATE_FORM_INVALID_ARGUMENTS');
115
+ } else if (args[i] === '--data-file') {
116
+ const next = args[i + 1];
117
+ if (dataFileInput !== undefined || !next || next.startsWith('--')) {
118
+ throwCreateFormError('--data-file requires exactly one value', 'CREATE_FORM_INVALID_ARGUMENTS');
119
+ }
120
+ dataFileInput = next;
121
+ args.splice(i, 2);
122
+ i--;
114
123
  } else if ((args[i] === '--locale' || args[i] === '--content-locale' || args[i] === '--lang') && i + 1 < args.length) {
115
124
  options.contentLocale = args[i + 1];
116
125
  if (!normalizeYidaLocale(options.contentLocale)) {
@@ -134,8 +143,10 @@ function createParseArgs(dependencies) {
134
143
  if (args.includes('--help') || args.includes('-h')) {
135
144
  usage(t('create_form.usage_create_short'));
136
145
  hint(t('create_form.usage_update_short'));
146
+ hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
137
147
  hint(t('create_form.example_create'));
138
148
  hint(t('create_form.example_update'));
149
+ hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
139
150
  return {
140
151
  mode: 'help',
141
152
  help: true,
@@ -145,6 +156,10 @@ function createParseArgs(dependencies) {
145
156
 
146
157
  const mode = args[0];
147
158
 
159
+ if (dataFileInput !== undefined && mode !== 'update') {
160
+ throwCreateFormError('--data-file is only supported by create-form update', 'CREATE_FORM_INVALID_ARGUMENTS');
161
+ }
162
+
148
163
  if (mode === 'icons' || mode === 'list-icons') {
149
164
  return {
150
165
  mode: 'icons',
@@ -182,7 +197,12 @@ function createParseArgs(dependencies) {
182
197
  }
183
198
 
184
199
  if (mode === 'update') {
185
- if (args.length < 4) {
200
+ const positionalInput = args[3];
201
+ if (args.length > 4 || (positionalInput && dataFileInput !== undefined)) {
202
+ throwCreateFormError('Use either <changesJsonOrFile> or --data-file, not both', 'CREATE_FORM_INVALID_ARGUMENTS');
203
+ }
204
+ const changesJsonOrFile = dataFileInput !== undefined ? dataFileInput : positionalInput;
205
+ if (args.length < 3 || !changesJsonOrFile) {
186
206
  usage(t('create_form.usage_update'), t('create_form.example_update'));
187
207
  throwCreateFormError(t('create_form.usage_update'), 'CREATE_FORM_INVALID_ARGUMENTS');
188
208
  }
@@ -190,7 +210,23 @@ function createParseArgs(dependencies) {
190
210
  mode: 'update',
191
211
  appType: args[1],
192
212
  formUuid: args[2],
193
- changesJsonOrFile: args[3],
213
+ changesJsonOrFile,
214
+ ...options
215
+ };
216
+ }
217
+
218
+ if (mode === 'resume') {
219
+ if (args.length !== 4) {
220
+ throwCreateFormError(
221
+ 'openyida create-form resume <appType> <formUuid> <fieldsJsonOrFile> --json',
222
+ 'CREATE_FORM_INVALID_ARGUMENTS'
223
+ );
224
+ }
225
+ return {
226
+ mode: 'resume',
227
+ appType: args[1],
228
+ formUuid: args[2],
229
+ fieldsJsonOrFile: args[3],
194
230
  ...options
195
231
  };
196
232
  }
@@ -313,8 +349,10 @@ function createParseArgs(dependencies) {
313
349
 
314
350
  usage(t('create_form.usage_create_short'));
315
351
  hint(t('create_form.usage_update_short'));
352
+ hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
316
353
  hint(t('create_form.example_create'));
317
354
  hint(t('create_form.example_update'));
355
+ hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
318
356
  throwCreateFormError(t('create_form.usage_create_short'), 'CREATE_FORM_INVALID_ARGUMENTS');
319
357
  };
320
358
  }
@@ -6,12 +6,50 @@ const crypto = require('crypto');
6
6
  const { execFile } = require('child_process');
7
7
  const { CliError } = require('../../core/cli-error');
8
8
  const { t } = require('../../core/i18n');
9
+ const { usage, hint } = require('../../core/chalk');
10
+ const { assertNoEmojiInText, assertNoEmojiInValue } = require('../../core/no-emoji-guard');
11
+ const { normalizeYidaLocale } = require('../../core/yida-i18n');
12
+ const { FORM_NAV_ICONS, resolveFormNavIcon } = require('./nav-icon');
13
+
14
+ const BATCH_USAGE = 'openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--check] [--json]';
15
+ const BATCH_PLAN_EXAMPLE = '{"forms":[{"key":"customer","title":"客户","fieldsFile":"customer-fields.json"},{"key":"order","title":"订单","fieldsFile":"order-fields.json","dependsOn":["customer"]}]}';
9
16
 
10
17
  function invalid(reason) {
11
18
  throw new CliError(t('create_form.batch_invalid'), { code: 'FORM_BATCH_INVALID', details: { reason } });
12
19
  }
13
20
 
21
+ function validateStaticDefinition(form, fields) {
22
+ try {
23
+ assertNoEmojiInText(form.title, { artifact: `forms.${form.key}.title` });
24
+ assertNoEmojiInValue(fields, { artifact: `forms.${form.key}.fields` });
25
+ } catch (error) {
26
+ invalid({
27
+ formKey: form.key,
28
+ errorCode: error?.code || 'OPENYIDA_ARTIFACT_EMOJI_FORBIDDEN',
29
+ errorMsg: error?.message || String(error),
30
+ });
31
+ }
32
+ if (form.icon !== undefined && !resolveFormNavIcon(form.icon, form.title, fields).icon) {
33
+ invalid({
34
+ formKey: form.key,
35
+ errorCode: 'CREATE_FORM_NAV_ICON_INVALID',
36
+ errorMsg: `Unsupported form navigation icon: ${form.icon}`,
37
+ icon: form.icon,
38
+ availableIconCount: FORM_NAV_ICONS.length,
39
+ });
40
+ }
41
+ if (form.locale !== undefined && !normalizeYidaLocale(form.locale)) {
42
+ invalid({
43
+ formKey: form.key,
44
+ errorCode: 'CREATE_FORM_INVALID_ARGUMENTS',
45
+ errorMsg: `Unsupported locale: ${form.locale}`,
46
+ locale: form.locale,
47
+ });
48
+ }
49
+ }
50
+
14
51
  function parseArgs(args) {
52
+ if (args.includes('--help') || args.includes('-h')) { return { help: true }; }
15
53
  const options = { concurrency: 3, check: false };
16
54
  const positional = [];
17
55
  for (let i = 0; i < args.length; i++) {
@@ -25,11 +63,19 @@ function parseArgs(args) {
25
63
  }
26
64
  }
27
65
  if (positional.length !== 2 || !Number.isInteger(options.concurrency) || options.concurrency < 1 || options.concurrency > 4) {
28
- invalid('create-form batch <appType> <plan.json> [--concurrency 1..4] [--check] [--json]');
66
+ invalid(BATCH_USAGE.replace(/^openyida /, ''));
29
67
  }
30
68
  return { ...options, appType: positional[0], file: path.resolve(positional[1]) };
31
69
  }
32
70
 
71
+ function showHelp() {
72
+ usage(BATCH_USAGE);
73
+ hint('plan.json: ' + BATCH_PLAN_EXAMPLE);
74
+ hint('fieldsFile is resolved relative to plan.json; use dependsOn and {"$form":"key","field":"label"} for dependencies.');
75
+ hint('Run one authoritative batch. If it continues in background, do not stop it, delete its state, or fall back to create/update/resume.');
76
+ return { success: true, help: true, usage: BATCH_USAGE };
77
+ }
78
+
33
79
  // References occupy a complete JSON value; ordinary text is preserved.
34
80
  function mapReferences(value, resolve) {
35
81
  if (Array.isArray(value)) { return value.map(item => mapReferences(item, resolve)); }
@@ -42,7 +88,38 @@ function mapReferences(value, resolve) {
42
88
  return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, mapReferences(item, resolve)]));
43
89
  }
44
90
 
45
- function loadPlan(file) {
91
+ function normalizeAssociationReferences(value, appType) {
92
+ if (Array.isArray(value)) {
93
+ return value.map(item => normalizeAssociationReferences(item, appType));
94
+ }
95
+ if (!value || typeof value !== 'object') { return value; }
96
+ const normalized = Object.fromEntries(Object.entries(value).map(([key, item]) => [
97
+ key,
98
+ normalizeAssociationReferences(item, appType),
99
+ ]));
100
+ const association = value.associationForm;
101
+ if (
102
+ value.type === 'AssociationFormField' &&
103
+ association &&
104
+ typeof association === 'object' &&
105
+ !Array.isArray(association) &&
106
+ typeof association.$form === 'string'
107
+ ) {
108
+ const reference = { $form: association.$form };
109
+ normalized.associationForm = {
110
+ ...(appType ? { appType } : {}),
111
+ formUuid: reference,
112
+ ...(typeof association.field === 'string' && association.field.trim() ? {
113
+ mainFieldId: { ...reference, field: association.field },
114
+ mainFieldLabel: association.field,
115
+ mainComponentName: 'TextField',
116
+ } : {}),
117
+ };
118
+ }
119
+ return normalized;
120
+ }
121
+
122
+ function loadPlan(file, options = {}) {
46
123
  const input = JSON.parse(fs.readFileSync(file, 'utf8'));
47
124
  if (!Array.isArray(input.forms) || !input.forms.length) { invalid('forms'); }
48
125
  const forms = input.forms.map(form => {
@@ -52,7 +129,9 @@ function loadPlan(file) {
52
129
  if (Object.keys(form).some(key => !['key', 'title', 'fields', 'fieldsFile', 'dependsOn', 'formUuid', 'icon', 'locale'].includes(key))) { invalid(form.key); }
53
130
  if (form.formUuid !== undefined && (typeof form.formUuid !== 'string' || !form.formUuid.startsWith('FORM'))) { invalid(form.key); }
54
131
  for (const key of ['icon', 'locale']) { if (form[key] !== undefined && typeof form[key] !== 'string') { invalid(key); } }
55
- const fields = form.fieldsFile === undefined ? form.fields : JSON.parse(fs.readFileSync(path.resolve(path.dirname(file), form.fieldsFile), 'utf8'));
132
+ const rawFields = form.fieldsFile === undefined ? form.fields : JSON.parse(fs.readFileSync(path.resolve(path.dirname(file), form.fieldsFile), 'utf8'));
133
+ const fields = normalizeAssociationReferences(rawFields, options.appType);
134
+ validateStaticDefinition(form, fields);
56
135
  const dependencies = new Set(form.dependsOn || []);
57
136
  mapReferences(fields, key => { dependencies.add(key); return 'FORM-PRECHECK'; });
58
137
  return { ...form, fields, dependsOn: [...dependencies] };
@@ -87,21 +166,85 @@ function parseOutput(stdout) {
87
166
  return output;
88
167
  }
89
168
 
90
- function execute(args) {
169
+ function mergeCommandOutput(stdout, stderr, failed) {
170
+ const stdoutOutput = parseOutput(stdout);
171
+ const stderrOutput = parseOutput(stderr);
172
+ if (!failed) { return stdoutOutput || stderrOutput; }
173
+ if (!stdoutOutput) { return stderrOutput; }
174
+ if (!stderrOutput) { return stdoutOutput; }
175
+ return { ...stdoutOutput, ...stderrOutput };
176
+ }
177
+
178
+ function boundedDiagnostic(value) {
179
+ const text = String(value || '').trim();
180
+ return text.length > 4000 ? text.slice(-4000) : text;
181
+ }
182
+
183
+ function expectedReadbackFields(fields) {
184
+ const expected = [];
185
+ for (const field of Array.isArray(fields) ? fields : []) {
186
+ if (Array.isArray(field)) {
187
+ expected.push(...expectedReadbackFields(field));
188
+ continue;
189
+ }
190
+ if (!field || typeof field !== 'object') { continue; }
191
+ if (field.type === 'ColumnContainer') {
192
+ expected.push(...expectedReadbackFields(field.children));
193
+ continue;
194
+ }
195
+ if (['Divider', 'RichText', 'PageSection', 'GroupContainer'].includes(field.type)) { continue; }
196
+ if (typeof field.label === 'string' && field.label.trim()) {
197
+ expected.push({ label: field.label.trim(), componentName: field.type });
198
+ }
199
+ }
200
+ return expected;
201
+ }
202
+
203
+ function readbackMatchesExpectedFields(schema, fields) {
204
+ if (!schema || typeof schema.formUuid !== 'string' || !Array.isArray(schema.fields)) { return false; }
205
+ const actual = schema.fields.filter(field => field && typeof field === 'object');
206
+ return expectedReadbackFields(fields).every(expected => actual.some(field =>
207
+ field.label === expected.label && (!expected.componentName || field.componentName === expected.componentName)
208
+ ));
209
+ }
210
+
211
+ function readbackMismatch(formUuid, form, schema) {
212
+ const expected = expectedReadbackFields(form.fields);
213
+ const actual = Array.isArray(schema?.fields) ? schema.fields.map(field => ({
214
+ label: field?.label,
215
+ componentName: field?.componentName,
216
+ })) : [];
217
+ return Object.assign(new Error(`Form batch readback mismatch: ${form.key}`), {
218
+ output: {
219
+ success: false,
220
+ errorCode: 'FORM_BATCH_READBACK_MISMATCH',
221
+ errorMsg: `Created form ${form.key} did not contain all expected fields`,
222
+ formUuid,
223
+ details: { formKey: form.key, formUuid, expected, actual },
224
+ },
225
+ });
226
+ }
227
+
228
+ function execute(args, { execFile: execFileImpl = execFile } = {}) {
91
229
  return new Promise((resolve, reject) => {
92
- execFile(process.execPath, [path.resolve(__dirname, '../../../bin/yida.js'), ...args, '--quiet'], {
230
+ execFileImpl(process.execPath, [path.resolve(__dirname, '../../../bin/yida.js'), ...args, '--quiet'], {
93
231
  env: { ...process.env, YIDA_QUIET: '1' }, timeout: 180000, maxBuffer: 8 * 1024 * 1024,
94
- }, (error, stdout) => {
95
- const output = parseOutput(stdout);
232
+ }, (error, stdout, stderr) => {
233
+ const output = mergeCommandOutput(stdout, stderr, Boolean(error));
96
234
  if (error || !output || typeof output !== 'object' || output.success === false) {
97
- reject(Object.assign(new Error(output?.errorMsg || output?.error || error?.message || 'Missing command result'), { output }));
235
+ reject(Object.assign(new Error(
236
+ output?.errorMsg || output?.error || output?.message || boundedDiagnostic(stderr) || error?.message || 'Missing command result'
237
+ ), { output }));
98
238
  } else { resolve(output); }
99
239
  });
100
240
  });
101
241
  }
102
242
 
103
243
  async function schedule(forms, concurrency, results, worker, save) {
104
- const pending = new Set(forms.filter(form => !results[form.key] || results[form.key].status === 'blocked').map(form => form.key));
244
+ const pending = new Set(forms.filter(form => {
245
+ const result = results[form.key];
246
+ return !result || result.status === 'blocked' || (result.status === 'failed' && result.formUuid);
247
+ }).map(form => form.key));
105
248
  const active = new Map();
106
249
  try {
107
250
  while (pending.size || active.size) {
@@ -114,7 +257,7 @@ async function schedule(forms, concurrency, results, worker, save) {
114
257
  }
115
258
  if (active.size >= concurrency || deps.some(status => status !== 'success')) { continue; }
116
259
  pending.delete(form.key);
117
- results[form.key] = { status: 'running' };
260
+ results[form.key] = { ...results[form.key], status: 'running' };
118
261
  save(); // Record intent before the request; an interrupted create is never retried automatically.
119
262
  const task = Promise.resolve().then(() => worker(form)).then(output => {
120
263
  results[form.key] = { ...output, status: 'success' };
@@ -137,7 +280,8 @@ async function schedule(forms, concurrency, results, worker, save) {
137
280
 
138
281
  async function run(args, dependencies = {}) {
139
282
  const options = parseArgs(args);
140
- const { forms, groups } = loadPlan(options.file);
283
+ if (options.help) { return showHelp(); }
284
+ const { forms, groups } = loadPlan(options.file, { appType: options.appType });
141
285
  const call = dependencies.execute || execute;
142
286
  // Preflight all definitions before the first remote mutation.
143
287
  await schedule(forms.map(form => ({ ...form, dependsOn: [] })), options.concurrency, {}, async form => {
@@ -178,21 +322,49 @@ async function run(args, dependencies = {}) {
178
322
  return matches[0].fieldId;
179
323
  };
180
324
  await schedule(forms, options.concurrency, state.results, async form => {
181
- let formUuid = form.formUuid;
325
+ let formUuid = form.formUuid || state.results[form.key]?.formUuid;
326
+ let shouldResume = Boolean(!form.formUuid && formUuid);
327
+ const resolvedFields = mapReferences(form.fields, resolve);
182
328
  if (!formUuid) {
183
- const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(mapReferences(form.fields, resolve)), '--no-open'];
329
+ const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(resolvedFields), '--no-open'];
184
330
  for (const key of ['icon', 'locale']) { if (form[key]) { argv.push(`--${key}`, form[key]); } }
185
- const created = await call(argv);
186
- if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
187
- formUuid = created.formUuid;
331
+ try {
332
+ const created = await call(argv);
333
+ if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
334
+ formUuid = created.formUuid;
335
+ } catch (error) {
336
+ const createdFormUuid = error.output?.formUuid || error.output?.details?.formUuid;
337
+ if (typeof createdFormUuid !== 'string' || !createdFormUuid.startsWith('FORM')) {
338
+ throw error;
339
+ }
340
+ formUuid = createdFormUuid;
341
+ shouldResume = true;
342
+ }
188
343
  }
189
344
  state.results[form.key].formUuid = formUuid;
190
345
  save(state);
191
- const schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
192
- if (schema.formUuid !== formUuid || !Array.isArray(schema.fields)) { invalid(`schema: ${form.key}`); }
346
+ let resumed = false;
347
+ if (shouldResume) {
348
+ await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
349
+ resumed = true;
350
+ }
351
+ let schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
352
+ if (!readbackMatchesExpectedFields(schema, resolvedFields) && !resumed && !form.formUuid) {
353
+ await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
354
+ resumed = true;
355
+ schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
356
+ }
357
+ if (schema.formUuid !== formUuid || !readbackMatchesExpectedFields(schema, resolvedFields)) {
358
+ throw readbackMismatch(formUuid, { ...form, fields: resolvedFields }, schema);
359
+ }
193
360
  return { formUuid, fields: schema.fields };
194
361
  }, () => save(state));
195
- const output = { success: Object.values(state.results).every(item => item.status === 'success'), groups, stateFile, results: state.results };
362
+ const success = Object.values(state.results).every(item => item.status === 'success');
363
+ const output = { success, groups, stateFile, results: state.results };
364
+ if (!success) {
365
+ output.errorCode = 'FORM_BATCH_PARTIAL_FAILURE';
366
+ output.nextAction = 'Inspect the saved state and child error, then fix the batch input or recover known formUuid values. Do not fall back to create-form create.';
367
+ }
196
368
  console.log(JSON.stringify(output));
197
369
  if (!output.success) { process.exitCode = 1; }
198
370
  return output;
@@ -201,4 +373,17 @@ async function run(args, dependencies = {}) {
201
373
  }
202
374
  }
203
375
 
204
- module.exports = { run, parseArgs, loadPlan, mapReferences, schedule, parseOutput };
376
+ module.exports = {
377
+ run,
378
+ parseArgs,
379
+ loadPlan,
380
+ mapReferences,
381
+ normalizeAssociationReferences,
382
+ schedule,
383
+ parseOutput,
384
+ mergeCommandOutput,
385
+ execute,
386
+ expectedReadbackFields,
387
+ readbackMatchesExpectedFields,
388
+ validateStaticDefinition,
389
+ };
@@ -6,6 +6,8 @@ function dispatchCreateFormCommand(parsedArgs, authContext, handlers) {
6
6
  switch (parsedArgs.mode) {
7
7
  case 'update':
8
8
  return handlers.update(...args);
9
+ case 'resume':
10
+ return handlers.resume(...args);
9
11
  case 'patch':
10
12
  return handlers.patch(...args);
11
13
  case 'rule':