openyida 2026.5.21 → 2026.5.25

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 (51) hide show
  1. package/README.md +5 -1
  2. package/bin/yida.js +7 -1
  3. package/lib/app/app-list.js +20 -1
  4. package/lib/app/check-page.js +2 -2
  5. package/lib/app/compile.js +3 -2
  6. package/lib/app/externalize-form.js +642 -0
  7. package/lib/app/import-app.js +39 -11
  8. package/lib/app/page-compat.js +258 -2
  9. package/lib/app/page-compiler.js +4 -1
  10. package/lib/app/page-linter.js +271 -0
  11. package/lib/app/publish.js +3 -2
  12. package/lib/auth/cdp-browser-login.js +7 -3
  13. package/lib/auth/login.js +2 -3
  14. package/lib/core/command-manifest.js +3 -0
  15. package/lib/core/copy.js +50 -8
  16. package/lib/core/env-manager.js +24 -16
  17. package/lib/core/locales/ar.js +7 -0
  18. package/lib/core/locales/de.js +7 -0
  19. package/lib/core/locales/en.js +7 -0
  20. package/lib/core/locales/es.js +7 -0
  21. package/lib/core/locales/fr.js +7 -0
  22. package/lib/core/locales/hi.js +7 -0
  23. package/lib/core/locales/ja.js +7 -0
  24. package/lib/core/locales/ko.js +7 -0
  25. package/lib/core/locales/pt.js +7 -0
  26. package/lib/core/locales/vi.js +7 -0
  27. package/lib/core/locales/zh-HK.js +7 -0
  28. package/lib/core/locales/zh.js +7 -0
  29. package/lib/core/utils.js +2 -2
  30. package/lib/process/configure-process.js +552 -20
  31. package/package.json +1 -1
  32. package/project/pages/src/demo-agent-chatbox.oyd.jsx +78 -3
  33. package/scripts/e2e-real/full-runner.js +257 -8
  34. package/scripts/e2e-real/skill-coverage.js +2 -2
  35. package/yida-skills/SKILL.md +1 -1
  36. package/yida-skills/skills/yida-chart/SKILL.md +1 -1
  37. package/yida-skills/skills/yida-create-process/SKILL.md +3 -2
  38. package/yida-skills/skills/yida-custom-page/SKILL.md +7 -2
  39. package/yida-skills/skills/yida-custom-page/examples/attachment-upload.js +14 -12
  40. package/yida-skills/skills/yida-custom-page/references/attachment-upload-guide.md +3 -1
  41. package/yida-skills/skills/yida-custom-page/references/coding-guide.md +4 -0
  42. package/yida-skills/skills/yida-custom-page/references/component-jsx-guide.md +31 -22
  43. package/yida-skills/skills/yida-dashboard/SKILL.md +10 -9
  44. package/yida-skills/skills/yida-dashboard/references/interaction-patterns.md +2 -0
  45. package/yida-skills/skills/yida-dashboard/references/pitfalls.md +13 -4
  46. package/yida-skills/skills/yida-dashboard/references/structure-and-layout.md +1 -1
  47. package/yida-skills/skills/yida-ppt-slider/SKILL.md +47 -37
  48. package/yida-skills/skills/yida-ppt-slider/references/examples.md +5 -4
  49. package/yida-skills/skills/yida-process-rule/SKILL.md +93 -3
  50. package/yida-skills/skills/yida-process-rule/references/official-component-nodes.md +93 -0
  51. package/yida-skills/skills/yida-publish-page/SKILL.md +6 -4
@@ -0,0 +1,642 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const {
6
+ loadCookieData,
7
+ triggerLogin,
8
+ resolveBaseUrl,
9
+ httpGet,
10
+ requestWithAutoLogin,
11
+ } = require('../core/utils');
12
+
13
+ const FIELD_COMPONENT_NAMES = new Set([
14
+ 'TextField',
15
+ 'TextareaField',
16
+ 'SelectField',
17
+ 'DateField',
18
+ 'NumberField',
19
+ 'RadioField',
20
+ 'CheckboxField',
21
+ 'EmployeeField',
22
+ 'PhoneField',
23
+ 'EmailField',
24
+ 'CascadeSelectField',
25
+ 'ImageField',
26
+ 'AttachmentField',
27
+ 'TableField',
28
+ 'MultiSelectField',
29
+ 'DepartmentSelectField',
30
+ 'AssociationFormField',
31
+ 'CountrySelectField',
32
+ 'CitySelectField',
33
+ 'RateField',
34
+ 'SignatureField',
35
+ 'SerialNumberField',
36
+ 'AddressField',
37
+ ]);
38
+
39
+ const PUBLIC_BLOCKED_TYPES = new Set([
40
+ 'AssociationFormField',
41
+ 'EmployeeField',
42
+ 'DepartmentSelectField',
43
+ ]);
44
+
45
+ const REVIEW_TYPES = new Set([
46
+ 'AttachmentField',
47
+ 'ImageField',
48
+ 'SignatureField',
49
+ 'TableField',
50
+ 'CascadeSelectField',
51
+ 'CountrySelectField',
52
+ 'CitySelectField',
53
+ 'AddressField',
54
+ ]);
55
+
56
+ const SAFE_CREATE_FORM_TYPES = new Set([
57
+ 'TextField',
58
+ 'TextareaField',
59
+ 'SelectField',
60
+ 'DateField',
61
+ 'NumberField',
62
+ 'RadioField',
63
+ 'CheckboxField',
64
+ 'PhoneField',
65
+ 'EmailField',
66
+ 'MultiSelectField',
67
+ 'RateField',
68
+ 'AttachmentField',
69
+ 'ImageField',
70
+ ]);
71
+
72
+ function readOption(args, names, fallback = '') {
73
+ const optionNames = Array.isArray(names) ? names : [names];
74
+ for (const name of optionNames) {
75
+ const index = args.indexOf(name);
76
+ if (index !== -1 && args[index + 1] && !args[index + 1].startsWith('--')) {
77
+ return args[index + 1];
78
+ }
79
+ }
80
+ return fallback;
81
+ }
82
+
83
+ function parseArgs(args) {
84
+ const parsed = {
85
+ appType: '',
86
+ formUuid: '',
87
+ schemaFile: '',
88
+ output: '',
89
+ mirrorFieldsOutput: '',
90
+ format: 'json',
91
+ target: 'open',
92
+ mirrorTitle: '',
93
+ help: false,
94
+ };
95
+ const positional = [];
96
+
97
+ for (let index = 0; index < args.length; index++) {
98
+ const arg = args[index];
99
+ if (arg === '--help' || arg === '-h') {
100
+ parsed.help = true;
101
+ } else if (arg === '--schema-file' || arg === '--schema') {
102
+ parsed.schemaFile = readOption(args, arg);
103
+ index++;
104
+ } else if (arg === '--output' || arg === '-o') {
105
+ parsed.output = readOption(args, arg);
106
+ index++;
107
+ } else if (arg === '--mirror-fields-output' || arg === '--fields-output') {
108
+ parsed.mirrorFieldsOutput = readOption(args, arg);
109
+ index++;
110
+ } else if (arg === '--format') {
111
+ parsed.format = readOption(args, arg, 'json');
112
+ index++;
113
+ } else if (arg === '--target') {
114
+ parsed.target = readOption(args, arg, 'open');
115
+ index++;
116
+ } else if (arg === '--mirror-title') {
117
+ parsed.mirrorTitle = readOption(args, arg);
118
+ index++;
119
+ } else if (arg.startsWith('--')) {
120
+ throw new Error(`Unknown option: ${arg}`);
121
+ } else {
122
+ positional.push(arg);
123
+ }
124
+ }
125
+
126
+ parsed.appType = positional[0] || '';
127
+ parsed.formUuid = positional[1] || '';
128
+ if (!['json', 'markdown'].includes(parsed.format)) {
129
+ throw new Error(`Unsupported format: ${parsed.format}`);
130
+ }
131
+ if (!['open', 'share'].includes(parsed.target)) {
132
+ throw new Error(`Unsupported target: ${parsed.target}`);
133
+ }
134
+ return parsed;
135
+ }
136
+
137
+ function printUsage() {
138
+ const lines = [
139
+ 'Usage: openyida externalize-form <appType> <formUuid> [options]',
140
+ '',
141
+ 'Options:',
142
+ ' --schema-file <file> Read a saved get-schema JSON file instead of fetching remotely',
143
+ ' --output, -o <file> Write the report to a file',
144
+ ' --mirror-fields-output <file> Write external-safe create-form fields JSON',
145
+ ' --format json|markdown Output format, default: json',
146
+ ' --target open|share Access target, default: open',
147
+ ' --mirror-title <title> Title used in suggested create-form command',
148
+ '',
149
+ 'Examples:',
150
+ ' openyida externalize-form APP_XXX FORM-XXX --schema-file .cache/schema.json',
151
+ ' openyida externalize-form APP_XXX FORM-XXX --mirror-fields-output .cache/external-fields.json',
152
+ ];
153
+ process.stderr.write(`${lines.join('\n')}\n`);
154
+ }
155
+
156
+ function extractText(value) {
157
+ if (value === null || value === undefined) {
158
+ return '';
159
+ }
160
+ if (typeof value === 'string') {
161
+ return value;
162
+ }
163
+ if (typeof value === 'number' || typeof value === 'boolean') {
164
+ return String(value);
165
+ }
166
+ if (Array.isArray(value)) {
167
+ return value.map(extractText).filter(Boolean).join(', ');
168
+ }
169
+ if (typeof value === 'object') {
170
+ return extractText(
171
+ value.zh_CN ||
172
+ value.en_US ||
173
+ value.ja_JP ||
174
+ value.value ||
175
+ value.text ||
176
+ value.label ||
177
+ value.name ||
178
+ value.title ||
179
+ ''
180
+ );
181
+ }
182
+ return '';
183
+ }
184
+
185
+ function isRequired(props) {
186
+ if (!props) {
187
+ return false;
188
+ }
189
+ if (props.required === true) {
190
+ return true;
191
+ }
192
+ const validation = Array.isArray(props.validation) ? props.validation : [];
193
+ return validation.some(rule => rule && rule.type === 'required');
194
+ }
195
+
196
+ function getPages(schemaResult) {
197
+ if (schemaResult && schemaResult.content && Array.isArray(schemaResult.content.pages)) {
198
+ return schemaResult.content.pages;
199
+ }
200
+ if (schemaResult && Array.isArray(schemaResult.pages)) {
201
+ return schemaResult.pages;
202
+ }
203
+ return [];
204
+ }
205
+
206
+ function collectFieldNodes(schemaResult) {
207
+ const fields = [];
208
+ const pages = getPages(schemaResult);
209
+
210
+ function traverse(node, parents) {
211
+ if (!node) {
212
+ return;
213
+ }
214
+ const props = node.props || {};
215
+ const label = extractText(props.label);
216
+ const nextParents = FIELD_COMPONENT_NAMES.has(node.componentName) && label
217
+ ? parents.concat(label)
218
+ : parents;
219
+
220
+ if (FIELD_COMPONENT_NAMES.has(node.componentName)) {
221
+ fields.push({
222
+ componentName: node.componentName,
223
+ props,
224
+ label,
225
+ fieldId: props.fieldId || '',
226
+ required: isRequired(props),
227
+ behavior: props.behavior || '',
228
+ path: nextParents.join(' > '),
229
+ });
230
+ }
231
+
232
+ const children = Array.isArray(node.children) ? node.children : [];
233
+ children.forEach(child => traverse(child, nextParents));
234
+ }
235
+
236
+ pages.forEach((page) => {
237
+ const roots = page && page.componentsTree ? page.componentsTree : [];
238
+ roots.forEach(root => traverse(root, []));
239
+ });
240
+
241
+ return fields;
242
+ }
243
+
244
+ function getRisk(field, target) {
245
+ if (target === 'share') {
246
+ if (field.componentName === 'AssociationFormField') {
247
+ return {
248
+ level: 'review',
249
+ reason: 'Association fields require internal form data permission and should be verified for org-share scenarios.',
250
+ };
251
+ }
252
+ return { level: 'safe', reason: 'Internal share keeps the visitor inside the organization permission boundary.' };
253
+ }
254
+
255
+ if (PUBLIC_BLOCKED_TYPES.has(field.componentName)) {
256
+ return {
257
+ level: 'blocked',
258
+ reason: `${field.componentName} depends on authenticated organization data and is not reliable for anonymous public access.`,
259
+ };
260
+ }
261
+ if (REVIEW_TYPES.has(field.componentName)) {
262
+ return {
263
+ level: 'review',
264
+ reason: `${field.componentName} may need a browser verification or a simpler external intake field.`,
265
+ };
266
+ }
267
+ return { level: 'safe', reason: 'Primitive input field, suitable for external intake.' };
268
+ }
269
+
270
+ function associationTarget(props) {
271
+ const associationForm = props && props.associationForm ? props.associationForm : {};
272
+ return {
273
+ appType: associationForm.appType || '',
274
+ formUuid: associationForm.formUuid || '',
275
+ formTitle: associationForm.formTitle || '',
276
+ mainFieldId: associationForm.mainFieldId || '',
277
+ mainFieldLabel: extractText(associationForm.mainFieldLabel),
278
+ };
279
+ }
280
+
281
+ function planField(field, target) {
282
+ const risk = getRisk(field, target);
283
+ const plan = {
284
+ label: field.label || field.fieldId || field.componentName,
285
+ fieldId: field.fieldId,
286
+ componentName: field.componentName,
287
+ required: field.required,
288
+ behavior: field.behavior,
289
+ path: field.path,
290
+ riskLevel: risk.level,
291
+ reason: risk.reason,
292
+ externalStrategy: 'keep',
293
+ };
294
+
295
+ if (field.componentName === 'AssociationFormField') {
296
+ plan.associationTarget = associationTarget(field.props);
297
+ plan.externalStrategy = 'snapshot-and-resolve-internal';
298
+ } else if (field.componentName === 'EmployeeField') {
299
+ plan.externalStrategy = 'collect-name-or-contact';
300
+ } else if (field.componentName === 'DepartmentSelectField') {
301
+ plan.externalStrategy = 'collect-department-text';
302
+ } else if (risk.level === 'review') {
303
+ plan.externalStrategy = 'verify-or-simplify';
304
+ }
305
+ return plan;
306
+ }
307
+
308
+ function mirrorLabel(label, suffix) {
309
+ if (!label) {
310
+ return suffix;
311
+ }
312
+ return `${label}${suffix}`;
313
+ }
314
+
315
+ function associationSnapshotLabel(plan) {
316
+ const target = plan.associationTarget || {};
317
+ const targetMainLabel = target.mainFieldLabel || '';
318
+ if (targetMainLabel) {
319
+ return targetMainLabel;
320
+ }
321
+ if (plan.label && plan.label.endsWith('名称')) {
322
+ return plan.label;
323
+ }
324
+ return mirrorLabel(plan.label, '名称');
325
+ }
326
+
327
+ function mirrorFieldForPlan(plan) {
328
+ const base = {
329
+ label: plan.label,
330
+ required: plan.required,
331
+ sourceFieldId: plan.fieldId,
332
+ sourceComponentName: plan.componentName,
333
+ sourceStrategy: plan.externalStrategy,
334
+ };
335
+
336
+ if (plan.componentName === 'AssociationFormField') {
337
+ const target = plan.associationTarget || {};
338
+ return [
339
+ {
340
+ type: 'TextField',
341
+ label: associationSnapshotLabel(plan),
342
+ required: plan.required,
343
+ sourceFieldId: plan.fieldId,
344
+ sourceComponentName: plan.componentName,
345
+ sourceStrategy: 'association-main-field-snapshot',
346
+ targetFormUuid: target.formUuid,
347
+ targetMainFieldId: target.mainFieldId,
348
+ },
349
+ {
350
+ type: 'TextField',
351
+ label: mirrorLabel(plan.label, '业务标识'),
352
+ required: false,
353
+ sourceFieldId: plan.fieldId,
354
+ sourceComponentName: plan.componentName,
355
+ sourceStrategy: 'association-business-key-for-internal-resolution',
356
+ targetFormUuid: target.formUuid,
357
+ },
358
+ ];
359
+ }
360
+
361
+ if (plan.componentName === 'EmployeeField') {
362
+ return [
363
+ { ...base, type: 'TextField', label: mirrorLabel(plan.label, '姓名') },
364
+ { ...base, type: 'TextField', label: mirrorLabel(plan.label, '联系方式'), required: false },
365
+ ];
366
+ }
367
+
368
+ if (plan.componentName === 'DepartmentSelectField') {
369
+ return [{ ...base, type: 'TextField', label: mirrorLabel(plan.label, '名称') }];
370
+ }
371
+
372
+ if (SAFE_CREATE_FORM_TYPES.has(plan.componentName)) {
373
+ return [{ ...base, type: plan.componentName }];
374
+ }
375
+
376
+ if (plan.componentName === 'SerialNumberField') {
377
+ return [];
378
+ }
379
+
380
+ return [{ ...base, type: 'TextareaField', label: mirrorLabel(plan.label, '说明'), required: false }];
381
+ }
382
+
383
+ function buildMirrorFields(fields) {
384
+ const mirrorFields = [];
385
+ fields.forEach((field) => {
386
+ mirrorFields.push(...mirrorFieldForPlan(field));
387
+ });
388
+ return mirrorFields;
389
+ }
390
+
391
+ function summarize(fields) {
392
+ const summary = {
393
+ totalFields: fields.length,
394
+ safeFields: 0,
395
+ reviewFields: 0,
396
+ blockedFields: 0,
397
+ associationFields: 0,
398
+ authBoundFields: 0,
399
+ };
400
+
401
+ fields.forEach((field) => {
402
+ if (field.riskLevel === 'safe') {
403
+ summary.safeFields++;
404
+ } else if (field.riskLevel === 'review') {
405
+ summary.reviewFields++;
406
+ } else if (field.riskLevel === 'blocked') {
407
+ summary.blockedFields++;
408
+ }
409
+ if (field.componentName === 'AssociationFormField') {
410
+ summary.associationFields++;
411
+ }
412
+ if (PUBLIC_BLOCKED_TYPES.has(field.componentName)) {
413
+ summary.authBoundFields++;
414
+ }
415
+ });
416
+ return summary;
417
+ }
418
+
419
+ function buildRecommendedActions(parsed, report) {
420
+ const actions = [
421
+ 'Create a separate external intake form from mirrorFields instead of exposing the internal form directly.',
422
+ 'Keep association fields in the internal form and resolve them after submission by business key or manual review.',
423
+ 'Copy human-readable snapshots into the external form so public visitors never need target-form permissions.',
424
+ ];
425
+
426
+ if (parsed.mirrorFieldsOutput) {
427
+ const title = parsed.mirrorTitle || `${report.formTitle || 'External Intake'} External`;
428
+ actions.unshift(
429
+ `Create mirror form: openyida create-form create ${parsed.appType} "${title}" ${parsed.mirrorFieldsOutput}`
430
+ );
431
+ }
432
+
433
+ if (report.summary.blockedFields === 0 && report.summary.reviewFields === 0) {
434
+ actions.unshift('No blocked fields were found; public access can still be verified with save-share-config and Chrome.');
435
+ }
436
+ return actions;
437
+ }
438
+
439
+ function inferFormTitle(schemaResult, formUuid) {
440
+ const content = schemaResult && schemaResult.content ? schemaResult.content : schemaResult;
441
+ return extractText(
442
+ content && (
443
+ content.formName ||
444
+ content.formTitle ||
445
+ content.title ||
446
+ content.name
447
+ )
448
+ ) || formUuid;
449
+ }
450
+
451
+ function analyzeSchema(schemaResult, options = {}) {
452
+ const target = options.target || 'open';
453
+ const fields = collectFieldNodes(schemaResult).map(field => planField(field, target));
454
+ const report = {
455
+ success: true,
456
+ appType: options.appType || '',
457
+ formUuid: options.formUuid || '',
458
+ formTitle: inferFormTitle(schemaResult, options.formUuid || ''),
459
+ target,
460
+ source: options.source || 'schema',
461
+ summary: summarize(fields),
462
+ fields,
463
+ mirrorFields: buildMirrorFields(fields),
464
+ recommendedActions: [],
465
+ notes: [
466
+ 'OpenYida cannot relax Yida platform permission boundaries for anonymous visitors.',
467
+ 'This plan keeps internal association fields private and generates external-safe snapshot fields.',
468
+ 'For fully automated sync, add an internal automation or API worker that resolves submitted business keys.',
469
+ ],
470
+ };
471
+ report.recommendedActions = buildRecommendedActions(options, report);
472
+ return report;
473
+ }
474
+
475
+ function renderMarkdown(report) {
476
+ const lines = [
477
+ `# External Access Plan: ${report.formTitle || report.formUuid}`,
478
+ '',
479
+ `- App: ${report.appType || '-'}`,
480
+ `- Form: ${report.formUuid || '-'}`,
481
+ `- Target: ${report.target}`,
482
+ `- Total fields: ${report.summary.totalFields}`,
483
+ `- Blocked fields: ${report.summary.blockedFields}`,
484
+ `- Review fields: ${report.summary.reviewFields}`,
485
+ '',
486
+ '## Fields',
487
+ '',
488
+ '| Label | Type | Risk | Strategy | Reason |',
489
+ '| --- | --- | --- | --- | --- |',
490
+ ];
491
+
492
+ report.fields.forEach((field) => {
493
+ lines.push(
494
+ `| ${field.label || '-'} | ${field.componentName} | ${field.riskLevel} | ${field.externalStrategy} | ${field.reason} |`
495
+ );
496
+ });
497
+
498
+ lines.push('', '## Recommended Actions', '');
499
+ report.recommendedActions.forEach(action => lines.push(`- ${action}`));
500
+ lines.push('', '## Mirror Fields', '', '```json');
501
+ lines.push(JSON.stringify(report.mirrorFields, null, 2));
502
+ lines.push('```', '');
503
+ return lines.join('\n');
504
+ }
505
+
506
+ function writeJson(filePath, data) {
507
+ const resolved = path.resolve(filePath);
508
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
509
+ fs.writeFileSync(resolved, JSON.stringify(data, null, 2), 'utf-8');
510
+ return resolved;
511
+ }
512
+
513
+ function writeText(filePath, text) {
514
+ const resolved = path.resolve(filePath);
515
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
516
+ fs.writeFileSync(resolved, text, 'utf-8');
517
+ return resolved;
518
+ }
519
+
520
+ function resolveOutputPath(filePath) {
521
+ const resolved = path.resolve(filePath);
522
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
523
+ return resolved;
524
+ }
525
+
526
+ function loadSchemaFile(filePath) {
527
+ const raw = fs.readFileSync(path.resolve(filePath), 'utf-8');
528
+ return JSON.parse(raw);
529
+ }
530
+
531
+ function createAuthRef() {
532
+ let cookieData = loadCookieData();
533
+ if (!cookieData) {
534
+ cookieData = triggerLogin();
535
+ }
536
+ return {
537
+ csrfToken: cookieData.csrf_token,
538
+ cookies: cookieData.cookies,
539
+ baseUrl: resolveBaseUrl(cookieData),
540
+ cookieData,
541
+ };
542
+ }
543
+
544
+ async function fetchSchema(appType, formUuid, authRef) {
545
+ return requestWithAutoLogin((auth) => {
546
+ return httpGet(
547
+ auth.baseUrl,
548
+ `/alibaba/web/${appType}/_view/query/formdesign/getFormSchema.json`,
549
+ { formUuid, schemaVersion: 'V5' },
550
+ auth.cookies
551
+ );
552
+ }, authRef);
553
+ }
554
+
555
+ function ensureSuccessfulSchema(result) {
556
+ if (!result || result.success === false || result.__needLogin || result.__csrfExpired) {
557
+ const message = result && (result.errorMsg || result.message)
558
+ ? result.errorMsg || result.message
559
+ : 'Failed to fetch form schema';
560
+ throw new Error(message);
561
+ }
562
+ }
563
+
564
+ async function loadSchema(parsed) {
565
+ if (parsed.schemaFile) {
566
+ return {
567
+ schema: loadSchemaFile(parsed.schemaFile),
568
+ source: path.resolve(parsed.schemaFile),
569
+ };
570
+ }
571
+ const authRef = createAuthRef();
572
+ const schema = await fetchSchema(parsed.appType, parsed.formUuid, authRef);
573
+ ensureSuccessfulSchema(schema);
574
+ return { schema, source: 'remote' };
575
+ }
576
+
577
+ async function run(args) {
578
+ let parsed;
579
+ try {
580
+ parsed = parseArgs(args || []);
581
+ } catch (error) {
582
+ console.error(error.message);
583
+ printUsage();
584
+ process.exit(1);
585
+ }
586
+
587
+ if (parsed.help) {
588
+ printUsage();
589
+ return;
590
+ }
591
+ if (!parsed.appType || !parsed.formUuid) {
592
+ printUsage();
593
+ process.exit(1);
594
+ }
595
+
596
+ try {
597
+ const loaded = await loadSchema(parsed);
598
+ const report = analyzeSchema(loaded.schema, {
599
+ ...parsed,
600
+ source: loaded.source,
601
+ });
602
+ const files = {};
603
+
604
+ if (parsed.mirrorFieldsOutput) {
605
+ files.mirrorFields = writeJson(parsed.mirrorFieldsOutput, report.mirrorFields);
606
+ }
607
+ if (parsed.output) {
608
+ files.report = resolveOutputPath(parsed.output);
609
+ }
610
+ report.files = files;
611
+ if (parsed.output) {
612
+ if (parsed.format === 'markdown') {
613
+ writeText(files.report, renderMarkdown(report));
614
+ } else {
615
+ writeJson(files.report, report);
616
+ }
617
+ }
618
+
619
+ const finalOutput = parsed.format === 'markdown'
620
+ ? renderMarkdown(report)
621
+ : JSON.stringify(report, null, 2);
622
+ console.log(finalOutput);
623
+ } catch (error) {
624
+ console.error(`externalize-form failed: ${error.message}`);
625
+ process.exit(1);
626
+ }
627
+ }
628
+
629
+ module.exports = {
630
+ parseArgs,
631
+ collectFieldNodes,
632
+ analyzeSchema,
633
+ renderMarkdown,
634
+ run,
635
+ _private: {
636
+ extractText,
637
+ isRequired,
638
+ planField,
639
+ buildMirrorFields,
640
+ summarize,
641
+ },
642
+ };