openyida 2026.10.9 → 2026.10.10-1

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 (24) hide show
  1. package/README.md +1 -1
  2. package/lib/app/update-app.js +157 -17
  3. package/lib/app/upgrade-app-theme.js +38 -11
  4. package/lib/core/command-manifest.js +9 -5
  5. package/lib/core/locales/en.js +9 -5
  6. package/lib/core/locales/zh.js +9 -5
  7. package/package.json +1 -1
  8. package/yida-skills/skills/yida-app/workflow/parallel-work.md +1 -1
  9. package/yida-skills/skills/yida-app/workflow/step-2-design.md +1 -1
  10. package/yida-skills/skills/yida-app/workflow/step-3-create-or-reuse-app.md +7 -7
  11. package/yida-skills/skills/yida-app/workflow/step-5-seed-records.md +2 -1
  12. package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +1 -1
  13. package/yida-skills/skills/yida-data-management/SKILL.md +12 -5
  14. package/yida-skills/skills/yida-design/SKILL.md +1 -1
  15. package/yida-skills/skills/yida-design/references/application-style-library.md +16 -29
  16. package/yida-skills/skills/yida-design/references/application-theme-consistency.md +3 -0
  17. package/yida-skills/skills/yida-design/references/native-form-styles.md +3 -0
  18. package/yida-skills/skills/yida-design/scripts/validate_design_themes.py +1 -2
  19. package/yida-skills/skills/yida-design/templates/design-themes/index.json +0 -252
  20. package/yida-skills/skills/yida-design/workflow/output-design.md +2 -0
  21. package/yida-skills/skills/yida-design/workflow/step-2-theme-system.md +1 -1
  22. package/yida-skills/skills/yida-design/workflow/step-6-handoff.md +3 -0
  23. package/yida-skills/skills/yida-upgrade-app-theme/SKILL.md +3 -3
  24. package/yida-skills/skills-index.json +1 -1
package/README.md CHANGED
@@ -403,7 +403,7 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
403
403
  | `openyida design-plan preview <build-plan.json> --part-file <module.json> [--json]` | Update plan drafts by module |
404
404
  | `openyida design-plan materialize <build-plan.json> [--from-preview \| --business-file <json> --visual-file <json>] [--output-dir <dir>] [--rebase-parts] [--check] [--json]` | Generate or update plan documents and theme with consistency checks |
405
405
  | `openyida design-plan patch <build-plan.json> --set <path=value> [--set <path=value> ...] [--materialize] [--output-dir <dir>] [--json]` | Patch plan fields and optionally sync documents and theme |
406
- | `openyida update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--nav-theme light\|dark\|white\|gray] [--logo-source appIcon\|customImage] [--layout side\|top\|l_shape] [--hide-app-nav\|--show-app-nav]` | Update app info |
406
+ | `openyida update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--confirm-legacy-app-style] [--disable-custom-theme] [--nav-theme light\|dark\|white\|gray] [--logo-source appIcon\|customImage] [--layout side\|top\|l_shape] [--hide-app-nav\|--show-app-nav]` | Update app info |
407
407
  | `openyida upgrade-app-theme <appType> --explicit-request [--confirm \| --prepare --output-dir <dir>] [--json]` | Upgrade an AI-built legacy app theme (explicit request and ask_human confirmation required) |
408
408
  | `openyida app-online <appType> [--to-ding-app-center] [--show-app-center]` | Enable a Yida app |
409
409
  | `openyida app-offline <appType> [--to-ding-app-center] [--show-app-center]` | Disable a Yida app |
@@ -14,6 +14,7 @@ const {
14
14
  } = require('../core/utils');
15
15
  const { createAuthRef } = require('../core/yida-client');
16
16
  const { t } = require('../core/i18n');
17
+ const { redactSensitive } = require('../core/redact');
17
18
  const { buildYidaI18n } = require('../core/yida-i18n');
18
19
  const { throwCommandError, throwUsage } = require('../core/command-errors');
19
20
  const { assertAppThemeKey } = require('./theme-presets');
@@ -81,6 +82,8 @@ function parseArgs(args) {
81
82
  themeColor: null,
82
83
  themeFile: null,
83
84
  customThemeStyle: null,
85
+ confirmLegacyAppStyle: false,
86
+ disableCustomTheme: false,
84
87
  navTheme: null,
85
88
  logoSource: null,
86
89
  layoutDirection: null,
@@ -100,7 +103,7 @@ function parseArgs(args) {
100
103
  for (let i = 1; i < args.length; i++) {
101
104
  const arg = args[i];
102
105
  const nextArg = args[i + 1];
103
- if (!['--hide-app-nav', '--show-app-nav'].includes(arg) &&
106
+ if (!['--hide-app-nav', '--show-app-nav', '--confirm-legacy-app-style', '--disable-custom-theme'].includes(arg) &&
104
107
  (!nextArg || nextArg.startsWith('--') || ['-n', '-d'].includes(nextArg))) {
105
108
  throwCommandError(t('cli_argument.invalid', arg), { code: 'CLI_INVALID_ARGUMENTS' });
106
109
  }
@@ -175,6 +178,12 @@ function parseArgs(args) {
175
178
  i++;
176
179
  }
177
180
  break;
181
+ case '--confirm-legacy-app-style':
182
+ result.confirmLegacyAppStyle = true;
183
+ break;
184
+ case '--disable-custom-theme':
185
+ result.disableCustomTheme = true;
186
+ break;
178
187
  case '--hide-app-nav':
179
188
  result.hideAppNav = 'y';
180
189
  break;
@@ -186,6 +195,7 @@ function parseArgs(args) {
186
195
  }
187
196
  }
188
197
 
198
+ assertCustomThemeOptions(result);
189
199
  return result;
190
200
  }
191
201
 
@@ -204,6 +214,7 @@ function hasShellUpdate(params) {
204
214
  params.colour ||
205
215
  params.themeColor ||
206
216
  params.themeFile ||
217
+ params.disableCustomTheme ||
207
218
  params.navTheme ||
208
219
  params.logoSource ||
209
220
  params.layoutDirection ||
@@ -240,6 +251,27 @@ function normalizeStoredLayoutDirection(layoutDirection, navType) {
240
251
  return 'side';
241
252
  }
242
253
 
254
+ /** 保存协议跟随应用实际主题能力;缺失运行态字段时保留既有保存行为。 */
255
+ function isLegacyNavigation(app) {
256
+ return pickAppField(app, 'appThemeEnable', 'APP_THEME_ENABLE') === 'n' ||
257
+ pickAppField(app, 'appThemeMode', 'APP_THEME_MODE') === 'legacy';
258
+ }
259
+
260
+ /** 旧 Shell 的布局协议与新版布局不同;保留已有旧值,不反写 navType。 */
261
+ function resolveSavedLayoutDirection(layoutDirection, navType, app, explicit = false) {
262
+ if (!isLegacyNavigation(app)) {
263
+ return explicit ? normalizeLayoutDirection(layoutDirection) : normalizeStoredLayoutDirection(layoutDirection, navType);
264
+ }
265
+ if (['slide', 'ver', 'hoz'].includes(layoutDirection)) {return layoutDirection;}
266
+ const layout = explicit ? normalizeLayoutDirection(layoutDirection) : normalizeStoredLayoutDirection(layoutDirection, navType);
267
+ return { side: 'ver', top: 'hoz', l_shape: 'ver' }[layout];
268
+ }
269
+
270
+ /** 旧导航没有白/灰独立风格,降级到已有浅色;不改应用品牌色和自定义 CSS。 */
271
+ function resolveSavedNavTheme(navTheme, app) {
272
+ return isLegacyNavigation(app) && ['white', 'gray'].includes(navTheme) ? 'light' : navTheme;
273
+ }
274
+
243
275
  function pickAppField(currentApp, fieldName, configName) {
244
276
  if (currentApp[fieldName] !== undefined && currentApp[fieldName] !== null) {
245
277
  return currentApp[fieldName];
@@ -302,7 +334,81 @@ function buildUpdateAppNamePostData(params, authRef) {
302
334
  return postDataObj;
303
335
  }
304
336
 
337
+ function assertCustomThemeOptions(params) {
338
+ const otherUpdates = ['name', 'desc', 'icon', 'iconColor', 'colour', 'themeColor', 'themeFile',
339
+ 'customThemeStyle', 'navTheme', 'logoSource', 'layoutDirection'];
340
+ if (params.disableCustomTheme && (otherUpdates.some(key => params[key]) ||
341
+ (params.hideAppNav !== null && params.hideAppNav !== undefined))) {
342
+ throwCommandError(t('update_app.disable_theme_conflict'), { code: 'CLI_INVALID_ARGUMENTS' });
343
+ }
344
+ }
345
+
346
+ function disabledCustomThemeStyle(rawStyle) {
347
+ let style = rawStyle;
348
+ if (typeof style === 'string') {
349
+ try {style = JSON.parse(style);} catch (_) {style = null;}
350
+ }
351
+ if (!style || Array.isArray(style) || typeof style !== 'object' ||
352
+ typeof style.cssUrl !== 'string' || !style.cssUrl.trim()) {
353
+ throwCommandError(t('update_app.custom_theme_config_invalid'), { code: 'APP_CUSTOM_THEME_CONFIG_INVALID' });
354
+ }
355
+ return JSON.stringify({ ...style, enabled: false, iframePropagation: false });
356
+ }
357
+
358
+ // A disable operation preserves the exact latest basic settings, including empty values.
359
+ function preserveBasicSettings(payload, app) {
360
+ const configKeys = {
361
+ colour: 'COLOUR', themeColor: 'THEME_COLOR', mode: 'APPMODE', type: 'APPTYPE', navLayout: 'NAVLAYOUT',
362
+ layoutDirection: 'LAY_OUT_DIRECTION', navType: 'NAVTYPE', navTheme: 'NAV_THEME', homepageLogo: 'HOMEPAGELOGO',
363
+ logoSource: 'LOGO_SOURCE', logoLink: 'LOGOLINK', systemLink: 'SYSTEMLINK', showAppTitle: 'SHOWAPPTITLE',
364
+ showIcon: 'SHOWICON', showNav: 'SHOWNAV', showCrumb: 'SHOWCRUMB', showAppCenter: 'SHOWAPPCENTER',
365
+ deviceType: 'DEVICETYPE', detailLayout: 'DETAIL_LAYOUT', procOperateRecordLayout: 'PROC_OPERATERECORD_LAYOUT',
366
+ navigation: 'NAVIGATION', pageHeader: 'PAGEHEADER', pageFooter: 'PAGEFOOTER',
367
+ addWaterMark: 'ADDWATERMARK', sentryMode: 'SENTRY_MODE', hideAppNav: 'HIDE_APP_NAV',
368
+ };
369
+ for (const [field, key] of Object.entries(configKeys)) {
370
+ const value = pickAppField(app, field, key);
371
+ if (value !== undefined && value !== null) {payload[field] = value;}
372
+ else {delete payload[field];}
373
+ }
374
+ for (const field of ['icon', 'iconUrl']) {
375
+ if (app[field] !== undefined && app[field] !== null) {payload[field] = app[field];}
376
+ }
377
+ }
378
+
379
+ function requireAppStyleConfirmation(params, context) {
380
+ const modern = pickAppField(context, 'appThemeEnable', 'APP_THEME_ENABLE') === 'y' &&
381
+ pickAppField(context, 'appThemeMode', 'APP_THEME_MODE') === 'modern';
382
+ if (!modern && params.confirmLegacyAppStyle !== true) {
383
+ throwCommandError(t('update_app.legacy_style_confirm_required'), {
384
+ code: 'LEGACY_APP_CUSTOM_THEME_CONFIRMATION_REQUIRED',
385
+ details: { appType: params.appType, applicationSettingsWritten: false, nextAction: 'ask_user_for_application_style_injection' },
386
+ });
387
+ }
388
+ }
389
+
390
+ // Runtime capability is resolved by the server. Stored APP_THEME_MODE alone is insufficient.
391
+ async function checkAppStyleInjection(params, authRef) {
392
+ const html = await requestWithAutoLogin(auth => httpGet(auth.baseUrl,
393
+ `/${encodeURIComponent(params.appType)}/admin`, { _stamp: Date.now() }, { responseType: 'text' }), authRef);
394
+ const { parseThemeContext } = require('./upgrade-app-theme');
395
+ // Even human approval cannot waive target application and organization identity.
396
+ const identity = parseThemeContext(html, ['appType', 'corpId']);
397
+ if (identity.appType !== params.appType || !authRef.corpId || identity.corpId !== authRef.corpId) {
398
+ throwCommandError(t('upgrade_app_theme.context_failed'), { code: 'APP_THEME_CONTEXT_UNVERIFIED' });
399
+ }
400
+ let context;
401
+ try {
402
+ context = parseThemeContext(html, ['appType', 'corpId', 'appThemeEnable', 'appThemeMode']);
403
+ } catch (error) {
404
+ if (error.code !== 'APP_THEME_CONTEXT_UNVERIFIED') {throw error;}
405
+ return requireAppStyleConfirmation(params, {});
406
+ }
407
+ requireAppStyleConfirmation(params, context);
408
+ }
409
+
305
410
  function buildUpdateAppPostData(params, currentApp = {}, authRef) {
411
+ assertCustomThemeOptions(params);
306
412
  assertAppThemeKey(params.colour);
307
413
  if (params.colour && params.colour !== 'custom' && (params.themeFile || params.customThemeStyle || params.themeColor)) {
308
414
  throw new Error(t('update_app.theme_preset_conflict'));
@@ -341,9 +447,8 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
341
447
  mode: currentApp.mode || (currentApp.config && currentApp.config.APPMODE) || 'normal',
342
448
  type: currentApp.type || (currentApp.config && currentApp.config.APPTYPE) || 'single',
343
449
  navLayout: currentApp.navLayout || (currentApp.config && currentApp.config.NAVLAYOUT) || 'auto',
344
- layoutDirection: params.layoutDirection
345
- ? normalizeLayoutDirection(params.layoutDirection)
346
- : normalizeStoredLayoutDirection(currentLayoutDirection, currentNavType),
450
+ layoutDirection: resolveSavedLayoutDirection(
451
+ params.layoutDirection || currentLayoutDirection, currentNavType, currentApp, !!params.layoutDirection),
347
452
  homepageLogo: currentApp.homepageLogo || (currentApp.config && currentApp.config.HOMEPAGELOGO) || '',
348
453
  logoSource: params.logoSource || currentApp.logoSource || (currentApp.config && currentApp.config.LOGO_SOURCE) || 'appIcon',
349
454
  logoLink: currentApp.logoLink || (currentApp.config && currentApp.config.LOGOLINK) || '',
@@ -370,7 +475,12 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
370
475
 
371
476
  // ThemeNavSetting 保存现有 navType;新布局由 layoutDirection 控制,不反写或补造旧字段。
372
477
  if (currentNavType) {postDataObj.navType = currentNavType;}
373
- const navTheme = params.navTheme || currentNavTheme;
478
+ if (params.disableCustomTheme && !params.layoutDirection) {
479
+ if (currentLayoutDirection) {postDataObj.layoutDirection = currentLayoutDirection;}
480
+ else {delete postDataObj.layoutDirection;}
481
+ }
482
+ const navTheme = params.disableCustomTheme && !params.navTheme
483
+ ? currentNavTheme : resolveSavedNavTheme(params.navTheme || currentNavTheme, currentApp);
374
484
  if (navTheme) {postDataObj.navTheme = navTheme;}
375
485
  if (addWaterMark !== undefined && addWaterMark !== null) {
376
486
  postDataObj.addWaterMark = addWaterMark;
@@ -394,7 +504,10 @@ function buildUpdateAppPostData(params, currentApp = {}, authRef) {
394
504
  postDataObj.themeColor = currentThemeColor;
395
505
  }
396
506
 
397
- if (params.colour && params.colour !== 'custom' && !params.customThemeStyle) {
507
+ if (params.disableCustomTheme) {
508
+ preserveBasicSettings(postDataObj, currentApp);
509
+ postDataObj.customThemeStyle = disabledCustomThemeStyle(currentCustomThemeStyle);
510
+ } else if (params.colour && params.colour !== 'custom' && !params.customThemeStyle) {
398
511
  // 切回平台预置主题时必须清空旧 CSS,避免 CUSTOM_THEME_STYLE 继续覆盖 colour。
399
512
  postDataObj.customThemeStyle = '';
400
513
  } else {
@@ -449,7 +562,7 @@ function themeSettingsMatch(expected, actual) {
449
562
  if (!expectedStyle || !actualStyle || typeof expectedStyle !== 'object' || typeof actualStyle !== 'object') {
450
563
  return expectedStyle === actualStyle;
451
564
  }
452
- return ['enabled', 'iframePropagation'].every((key) => Boolean(expectedStyle[key]) === Boolean(actualStyle[key])) &&
565
+ return ['enabled', 'iframePropagation'].every((key) => expectedStyle[key] === actualStyle[key]) &&
453
566
  ['cssUrl', 'cssFileName'].every((key) => (expectedStyle[key] || '') === (actualStyle[key] || ''));
454
567
  }
455
568
 
@@ -459,14 +572,16 @@ function getRequestedNavigationSettings(params, payload) {
459
572
  .map((name) => [name, payload[name]]));
460
573
  }
461
574
 
462
- function getNavigationSettings(app, fields) {
463
- const configKeys = { navTheme: 'NAV_THEME', layoutDirection: 'LAY_OUT_DIRECTION', hideAppNav: 'HIDE_APP_NAV', logoSource: 'LOGO_SOURCE' };
575
+ function getNavigationSettings(app, fields, currentApp = app, rawLayout = false) {
576
+ const configKeys = { navTheme: 'NAV_THEME', layoutDirection: 'LAY_OUT_DIRECTION', hideAppNav: 'HIDE_APP_NAV', logoSource: 'LOGO_SOURCE', navType: 'NAVTYPE', homepageLogo: 'HOMEPAGELOGO' };
464
577
  return Object.fromEntries(fields.map((name) => {
465
578
  let value = pickAppField(app, name, configKeys[name]);
466
- if (name === 'layoutDirection') {
579
+ if (name === 'layoutDirection' && !rawLayout) {
467
580
  const navType = pickAppField(app, 'navType', 'NAVTYPE');
468
581
  // A missing response field is not evidence that the requested default was saved.
469
- value = value || navType ? normalizeStoredLayoutDirection(value, navType) : null;
582
+ value = isLegacyNavigation(currentApp)
583
+ ? (value || null)
584
+ : (value || navType ? normalizeStoredLayoutDirection(value, navType) : null);
470
585
  }
471
586
  return [name, value === undefined ? null : value];
472
587
  }));
@@ -474,18 +589,28 @@ function getNavigationSettings(app, fields) {
474
589
 
475
590
  /** updateApp 整包保存;读取最新配置后合并,并核验主题资源和本次请求的导航设置。 */
476
591
  async function saveAppSettings(params, authRef) {
592
+ assertCustomThemeOptions(params);
593
+ if (params.customThemeStyle) {await checkAppStyleInjection(params, authRef);}
477
594
  const currentApp = await fetchCurrentApp(params.appType, authRef);
478
595
  const payload = buildUpdateAppPostData(params, currentApp, authRef);
596
+ const submittedFields = params.disableCustomTheme ? redactSensitive({
597
+ customThemeStyle: JSON.parse(payload.customThemeStyle), colour: payload.colour, themeColor: payload.themeColor,
598
+ navType: payload.navType, navTheme: payload.navTheme, layoutDirection: payload.layoutDirection,
599
+ logoSource: payload.logoSource, homepageLogo: payload.homepageLogo,
600
+ }) : undefined;
479
601
  const response = await requestWithAutoLogin((auth) => httpPost(
480
602
  auth.baseUrl,
481
603
  `/${params.appType}/query/app/updateApp.json?_api=Form.updateApp&_mock=false&_stamp=${Date.now()}`,
482
604
  querystring.stringify({ ...payload, _csrf_token: auth.csrfToken })
483
605
  ), authRef);
484
- if (!response || !response.success) { return response; }
606
+ if (!response || !response.success) { return submittedFields ? { ...response, submittedFields } : response; }
485
607
 
486
608
  const expected = getThemeSettings(payload);
487
- const verifyTheme = !!(params.colour || params.themeColor || params.customThemeStyle || expected.themeColor || expected.customThemeStyle);
488
- const expectedNavigation = getRequestedNavigationSettings(params, payload);
609
+ const verifyTheme = !!(params.colour || params.themeColor || params.customThemeStyle || params.disableCustomTheme || expected.themeColor || expected.customThemeStyle);
610
+ const expectedNavigation = params.disableCustomTheme
611
+ ? Object.fromEntries(['navType', 'navTheme', 'layoutDirection', 'hideAppNav', 'logoSource', 'homepageLogo']
612
+ .filter(field => Object.hasOwn(payload, field)).map(field => [field, payload[field]]))
613
+ : getRequestedNavigationSettings(params, payload);
489
614
  const navigationFields = Object.keys(expectedNavigation);
490
615
  if (!verifyTheme && !navigationFields.length) {return response;}
491
616
  let actual;
@@ -499,12 +624,13 @@ async function saveAppSettings(params, authRef) {
499
624
  try {
500
625
  const app = await fetchCurrentApp(params.appType, authRef);
501
626
  actual = getThemeSettings(app);
502
- actualNavigation = getNavigationSettings(app, navigationFields);
627
+ actualNavigation = getNavigationSettings(app, navigationFields, currentApp, params.disableCustomTheme);
503
628
  readbackError = undefined;
504
629
  themeVerified = !verifyTheme || themeSettingsMatch(expected, actual);
505
630
  navigationVerified = navigationFields.every((name) => expectedNavigation[name] === actualNavigation[name]);
506
631
  if (themeVerified && navigationVerified) {
507
632
  return { ...response,
633
+ ...(submittedFields ? { submittedFields } : {}),
508
634
  ...(verifyTheme ? { themeVerification: { verified: true, ...actual } } : {}),
509
635
  ...(navigationFields.length ? { navigationVerification: { verified: true, ...actualNavigation } } : {}),
510
636
  };
@@ -517,6 +643,7 @@ async function saveAppSettings(params, authRef) {
517
643
  }
518
644
  return {
519
645
  success: false,
646
+ ...(submittedFields ? { submittedFields } : {}),
520
647
  errorCode: themeVerified ? 'APP_NAVIGATION_NOT_PERSISTED' : 'APP_THEME_NOT_PERSISTED',
521
648
  errorMsg: t(themeVerified ? 'update_app.navigation_not_persisted' : 'update_app.theme_not_persisted'),
522
649
  ...(verifyTheme ? { themeVerification: themeVerified
@@ -536,6 +663,8 @@ async function applyCustomThemeUpdate(appType, themeParams, authRef) {
536
663
  assertNavTheme(params.navTheme);
537
664
  assertLogoSource(params.logoSource);
538
665
 
666
+ assertCustomThemeOptions(params);
667
+ await checkAppStyleInjection(params, authRef);
539
668
  const themeFile = readThemeCssFile(params.themeFile);
540
669
  params.themeColor = themeFile.themeColor;
541
670
  const uploadResponse = await requestWithAutoLogin(
@@ -544,7 +673,13 @@ async function applyCustomThemeUpdate(appType, themeParams, authRef) {
544
673
  );
545
674
  params.customThemeStyle = buildCustomThemeStyle(uploadResponse);
546
675
 
547
- return saveAppSettings(params, authRef);
676
+ try {
677
+ return await saveAppSettings(params, authRef);
678
+ } catch (error) {
679
+ error.details = { ...error.details, appType, uploadedResource: params.customThemeStyle,
680
+ applicationSettingsVerified: false };
681
+ throw error;
682
+ }
548
683
  }
549
684
 
550
685
  async function run(args) {
@@ -570,6 +705,7 @@ async function run(args) {
570
705
  !params.colour &&
571
706
  !params.themeColor &&
572
707
  !params.themeFile &&
708
+ !params.disableCustomTheme &&
573
709
  !params.navTheme &&
574
710
  !params.logoSource &&
575
711
  !params.layoutDirection &&
@@ -579,6 +715,7 @@ async function run(args) {
579
715
  printUsage();
580
716
  throwUsage(t('update_app.missing_update_field'), t('update_app.usage'));
581
717
  }
718
+ assertCustomThemeOptions(params);
582
719
  assertAppThemeKey(params.colour);
583
720
  if (params.colour && params.colour !== 'custom' && (params.themeFile || params.themeColor)) {
584
721
  throw new Error(t('update_app.theme_preset_conflict'));
@@ -635,13 +772,14 @@ async function run(args) {
635
772
  if (response && response.success) {
636
773
  const details = [['appType', params.appType]];
637
774
  if (params.name) {details.push(['Name', params.name]);}
638
- chalkResult(true, t('update_app.success'), details);
775
+ chalkResult(true, t(params.disableCustomTheme ? 'update_app.custom_theme_disabled' : 'update_app.success'), details);
639
776
 
640
777
  console.log(JSON.stringify({
641
778
  success: true,
642
779
  appType: params.appType,
643
780
  themeVerification: response.themeVerification,
644
781
  navigationVerification: response.navigationVerification,
782
+ ...(params.disableCustomTheme ? { runtimeVerification: { verified: false } } : {}),
645
783
  updatedFields: {
646
784
  name: params.name || undefined,
647
785
  desc: params.desc || undefined,
@@ -649,6 +787,7 @@ async function run(args) {
649
787
  colour: params.colour || undefined,
650
788
  themeColor: params.themeColor || undefined,
651
789
  themeFile: params.themeFile || undefined,
790
+ customThemeStyle: params.disableCustomTheme ? response.themeVerification.customThemeStyle : undefined,
652
791
  navTheme: params.navTheme || undefined,
653
792
  logoSource: params.logoSource || undefined,
654
793
  layoutDirection: params.layoutDirection || undefined,
@@ -659,6 +798,7 @@ async function run(args) {
659
798
  const errorMsg = response ? response.errorMsg || t('common.unknown_error') : t('common.request_failed');
660
799
  chalkResult(false, t('update_app.failed', errorMsg));
661
800
  console.log(JSON.stringify({ success: false, appType: params.appType, error: errorMsg,
801
+ submittedFields: response && response.submittedFields,
662
802
  errorCode: response && response.errorCode, themeVerification: response && response.themeVerification,
663
803
  navigationVerification: response && response.navigationVerification }));
664
804
  throwCommandError(errorMsg, { code: response && response.errorCode,
@@ -24,7 +24,7 @@ function parseArgs(args = []) {
24
24
  }
25
25
 
26
26
  // Read only literal fields from the server-rendered admin configuration. Never execute HTML/JS.
27
- function parseThemeContext(html) {
27
+ function parseThemeContext(html, wanted = ['appType', 'corpId', 'agentAppType']) {
28
28
  const fail = () => throwCommandError(t('upgrade_app_theme.context_failed'), { code: 'APP_THEME_CONTEXT_UNVERIFIED' });
29
29
  if (typeof html !== 'string') { return fail(); }
30
30
  const contexts = [];
@@ -40,7 +40,6 @@ function parseThemeContext(html) {
40
40
  expr.left.object.name !== 'window' || expr.left.property.name !== 'pageConfig') { continue; }
41
41
  if (expr.right.type !== 'ObjectExpression') { return fail(); }
42
42
  const context = {};
43
- const wanted = ['appType', 'corpId', 'agentAppType'];
44
43
  for (const prop of expr.right.properties) {
45
44
  if (prop.type === 'SpreadElement' || prop.computed) { return fail(); }
46
45
  const key = prop.key.name || prop.key.value;
@@ -71,17 +70,34 @@ async function readThemeEligibility(appType, auth) {
71
70
  return context;
72
71
  }
73
72
 
73
+ /** 升级时将历史导航协议转换为新版值,保留导航结构与已配置的新值。 */
74
+ function resolveModernNavigationConfig(previous) {
75
+ const layout = previous.LAY_OUT_DIRECTION;
76
+ const navType = previous.NAVTYPE;
77
+ const targets = {};
78
+ // 旧版 ver 默认是侧导;只有明确的 top_side 结构才恢复 L 型。
79
+ const layouts = { slide: 'side', ver: navType === 'top_side' ? 'l_shape' : 'side',
80
+ hoz: navType === 'top_side' ? 'l_shape' : 'top' };
81
+ if (Object.hasOwn(layouts, layout)) {targets.LAY_OUT_DIRECTION = layouts[layout];}
82
+ // APP_THEME_MODE=modern 的 light 已是新版浅品牌色,重复升级不能把它变成 white。
83
+ const wasLegacy = previous.APP_THEME_MODE !== 'modern';
84
+ const colors = { default: 'light', light: 'white', dark: 'dark', clear: 'white' };
85
+ if (wasLegacy && Object.hasOwn(colors, previous.NAV_THEME)) {targets.NAV_THEME = colors[previous.NAV_THEME];}
86
+ return targets;
87
+ }
88
+
74
89
  // Both attestations are required even for callers that bypass the CLI runner.
75
90
  // They record the agent's authorization checks; they cannot prove a human reply.
76
91
  async function upgradeAppTheme(params, authRef) {
77
92
  if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType)) {
78
93
  throwUsage(t('upgrade_app_theme.usage'));
79
94
  }
95
+ const auth = authRef || createAuthRef();
96
+ // Reject ineligible apps before offering upgrade confirmation; flags cannot bypass this gate.
97
+ await readThemeEligibility(params.appType, auth);
80
98
  if (params.explicitRequest !== true || params.confirm !== true) {
81
99
  throwCommandError(t('upgrade_app_theme.confirm_required'), { code: 'APP_THEME_CONFIRMATION_REQUIRED' });
82
100
  }
83
- const auth = authRef || createAuthRef();
84
- await readThemeEligibility(params.appType, auth);
85
101
  const targets = { CREATED_WITH_MODERN_THEME: 'y', APP_THEME_MODE: 'modern' };
86
102
  const request = (endpoint, fields) => requestWithAutoLogin((current) => httpPost(
87
103
  current.baseUrl,
@@ -97,13 +113,18 @@ async function upgradeAppTheme(params, authRef) {
97
113
  }
98
114
  return emptyContent ? null : response.content;
99
115
  };
100
- // Read both before writing anything. The two endpoints are not transactional.
116
+ // Read theme and navigation before writing anything; these endpoints are not transactional.
101
117
  const previous = {};
102
- for (const key of Object.keys(targets)) { previous[key] = await readConfig(key); }
118
+ for (const key of [...Object.keys(targets), 'LAY_OUT_DIRECTION', 'NAV_THEME', 'NAVTYPE']) {
119
+ previous[key] = await readConfig(key);
120
+ }
121
+ const navigationTargets = resolveModernNavigationConfig(previous);
122
+ // 先写导航,再启用主题,避免配色/布局迁移失败后留下已启用的新主题。
123
+ const allTargets = { ...navigationTargets, ...targets };
103
124
  const verifiedWrites = [];
104
125
  let attemptedKey;
105
126
  try {
106
- for (const [key, value] of Object.entries(targets)) {
127
+ for (const [key, value] of Object.entries(allTargets)) {
107
128
  if (previous[key] === value) { continue; }
108
129
  attemptedKey = key;
109
130
  const response = await request('updateSingleConfig', { key, value });
@@ -115,9 +136,9 @@ async function upgradeAppTheme(params, authRef) {
115
136
  }
116
137
  verifiedWrites.push(key);
117
138
  }
118
- // Verify the pair again after writes; never claim both succeeded from only one response.
139
+ // Verify all migration targets again after writes; each value needs independent readback.
119
140
  if (attemptedKey) {
120
- for (const [key, value] of Object.entries(targets)) {
141
+ for (const [key, value] of Object.entries(allTargets)) {
121
142
  if (await readConfig(key) !== value) {
122
143
  throwCommandError(t('upgrade_app_theme.verify_failed'), { code: 'APP_THEME_NOT_PERSISTED' });
123
144
  }
@@ -133,7 +154,10 @@ async function upgradeAppTheme(params, authRef) {
133
154
  return { success: true, appType: params.appType, changed: verifiedWrites.length > 0,
134
155
  verified: true, verificationScope: 'config', runtimeVerified: false, appUpgradeComplete: false,
135
156
  nextStep: 'migrate-pages-and-clean-form-styles-with-yida-upgrade-app-theme', previous,
136
- createdWithModernTheme: 'y', appThemeMode: 'modern', updatedKeys: verifiedWrites };
157
+ createdWithModernTheme: 'y', appThemeMode: 'modern', navigation: {
158
+ layoutDirection: navigationTargets.LAY_OUT_DIRECTION || previous.LAY_OUT_DIRECTION,
159
+ navTheme: navigationTargets.NAV_THEME || previous.NAV_THEME,
160
+ }, updatedKeys: verifiedWrites };
137
161
 
138
162
  }
139
163
 
@@ -145,11 +169,14 @@ async function run(args = []) {
145
169
  return;
146
170
  }
147
171
  if (params.prepare) {
148
- if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType) || !params.explicitRequest || !params.outputDir) {
172
+ if (!params.appType || !/^[A-Za-z0-9_-]+$/.test(params.appType)) {
149
173
  throwUsage(t('upgrade_app_theme.prepare_output_required'));
150
174
  }
151
175
  const auth = createAuthRef();
152
176
  await readThemeEligibility(params.appType, auth);
177
+ if (!params.explicitRequest || !params.outputDir) {
178
+ throwUsage(t('upgrade_app_theme.prepare_output_required'));
179
+ }
153
180
  const { prepareAppThemeUpgrade } = require('./prepare-app-theme-upgrade');
154
181
  const prepared = await prepareAppThemeUpgrade(params, auth);
155
182
  if (!prepared.success) {
@@ -1139,7 +1139,7 @@ const COMMAND_GROUPS = [
1139
1139
  { name: 'json', type: 'boolean', source: 'option', builder_options: ['--json'] },
1140
1140
  ],
1141
1141
  }),
1142
- command('update-app', ['update-app'], 'update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape] [--hide-app-nav|--show-app-nav]', 'help.cmd_update_app', {
1142
+ command('update-app', ['update-app'], 'update-app <appType> [--name "..."] [--desc "..."] [--icon <name>] [--icon-color <color>] [--colour <key>] [--theme-color <color>] [--theme-file <css>] [--confirm-legacy-app-style] [--disable-custom-theme] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape] [--hide-app-nav|--show-app-nav]', 'help.cmd_update_app', {
1143
1143
  args: [
1144
1144
  { name: 'appType', type: 'string', required: true, source: 'positional', position: 0 },
1145
1145
  { name: 'name', type: 'string', source: 'option', builder_options: ['--name', '-n'] },
@@ -1149,15 +1149,19 @@ const COMMAND_GROUPS = [
1149
1149
  { name: 'colour', type: 'string', source: 'option', builder_options: ['--colour', '--theme'], description: 'Platform theme key or custom; CSS colors belong in themeColor.' },
1150
1150
  { name: 'themeColor', type: 'string', source: 'option', builder_options: ['--theme-color', '--themeColor'], description: 'App primary color; themeFile supplies the effective color when both are provided.' },
1151
1151
  { name: 'themeFile', type: 'file', source: 'option', builder_options: ['--theme-file', '--custom-theme-file'], description: 'Completed application theme CSS, not a placeholder authoring bundle. Reads --color-brand1-6, uploads CSS and sets colour=custom. Supports app-theme.css or app_theme.css; use the actual generated path.' },
1152
- { name: 'navTheme', type: 'string', source: 'option', builder_options: ['--nav-theme', '--navTheme'], description: 'Platform value: light, dark, white or gray. Generated design flows pass the selected template-derived light/dark value explicitly; this option has no implicit light default.' },
1152
+ { name: 'confirmLegacyAppStyle', type: 'boolean', source: 'option', builder_options: ['--confirm-legacy-app-style'], description: 'Attests to an affirmative human reply for this exact legacy/unknown app and reviewed CSS scope; never infer consent from a request to beautify a page.' },
1153
+ { name: 'disableCustomTheme', type: 'boolean', source: 'option', builder_options: ['--disable-custom-theme'], description: 'Disable app-level CSS by serializing the existing customThemeStyle with boolean enabled=false and iframePropagation=false. Retain cssUrl/cssFileName and other basic settings. Cannot be combined with other updates.' },
1154
+ { name: 'navTheme', type: 'string', source: 'option', builder_options: ['--nav-theme', '--navTheme'], description: 'Platform value: light, dark, white or gray; disabled/legacy apps persist white/gray as light. Generated design flows pass the selected template-derived light/dark value explicitly; this option has no implicit light default.' },
1153
1155
  { name: 'logoSource', type: 'string', source: 'option', builder_options: ['--logo-source', '--logoSource'], description: 'appIcon or customImage; customImage requires an existing homepageLogo.' },
1154
- { name: 'layoutDirection', type: 'string', source: 'option', builder_options: ['--layout', '--layout-direction', '--layoutDirection'], description: 'side, top or l_shape.' },
1156
+ { name: 'layoutDirection', type: 'string', source: 'option', builder_options: ['--layout', '--layout-direction', '--layoutDirection'], description: 'side, top or l_shape. Disabled or legacy apps persist ver, hoz or ver respectively.' },
1155
1157
  { name: 'hideAppNav', type: 'boolean', source: 'option', builder_options: ['--hide-app-nav'] },
1156
1158
  { name: 'showAppNav', type: 'boolean', source: 'option', builder_options: ['--show-app-nav'] },
1157
1159
  ],
1158
1160
  notes: [
1161
+ 'Before uploading or binding CSS, read server-resolved appThemeEnable/appThemeMode and verify app/corp identity. Only y/modern allows automatic app-level injection. Legacy or unknown capability requires asking the human about this exact application and reviewed stylesheet scope before --confirm-legacy-app-style; unavailable confirmation means stop before uploading. Prefer page-scoped styles for legacy applications. --disable-custom-theme retains the existing resource URL/name and sets enabled/iframePropagation to boolean false. Never submit an empty style or remove the resource URL. Inspect strict configuration readback, then refresh the actual visit page to verify link#yida-app-custom-theme is absent and the CSS is not requested; HTTP success alone is insufficient.',
1159
1162
  'Upload application CSS with --theme-file; the primary color is read from CSS and saved with the theme resource. Edit navigation radius, three border states and selected shadow in design.md, regenerate CSS, then upload; these tokens are not separate update-app flags. Unknown options, missing values and combined --hide-app-nav/--show-app-nav are rejected.',
1160
1163
  'One design document and theme CSS cover navigation, application shell, native forms, custom pages and record details. tokens.application-global.appearance.navigation defines navigation surfaces, text and interaction states, logo, groups, search, popups, borders, radius and density. Platform-supported tokens are applied through the active navigation mode; page content consumes the same application theme.',
1164
+ 'Applications with appThemeEnable=n or appThemeMode=legacy persist side/top/l_shape as ver/hoz/ver and map unsupported white/gray navigation colors to legacy light. Existing legacy layouts and navType are preserved. Unknown theme capability keeps the existing save protocol.',
1161
1165
  'navigationVerification reads back explicitly requested navTheme, layoutDirection, hideAppNav and logoSource. A mismatch returns APP_NAVIGATION_NOT_PERSISTED and a nonzero exit code; only readback is retried. themeVerification independently verifies the uploaded theme resource. Verify rendered navigation, form, custom page and record detail after both checks pass.',
1162
1166
  'For generated design artifacts, pass themeProfile.navTheme from design.md to --nav-theme. Do not infer it from the business brief or omit it expecting a light default.',
1163
1167
  'Platform preset keys are used independently of --theme-file and --theme-color. Choose --hide-app-nav or --show-app-nav to set navigation visibility.',
@@ -1173,7 +1177,7 @@ const COMMAND_GROUPS = [
1173
1177
  { name: 'json', type: 'boolean', source: 'option', builder_options: ['--json'] },
1174
1178
  { name: 'confirm', type: 'boolean', source: 'option', builder_options: ['--confirm'] },
1175
1179
  ],
1176
- notes: ['--prepare reads remote schemas/source bundles into a new local backup directory and produces a per-resource migration plan without remote writes; it requires explicit upgrade intent but no mutation confirmation. Before --confirm, use the independent upgrade Skill to prepare/review sources, migrate legacy JSX to Canvas without business/layout changes, and remove verified form-detail style injection. Only mutate after an explicit user request to upgrade an existing app to the modern theme AND an affirmative ask_human reply for that app. These flags attest to both checks; they do not replace human confirmation. If ask_human is unavailable, stop before mutation. Only agentAppType=local/cloud/qwenwork qualifies; verify target app/corp identity. Do not check V4, appThemeEnable, organization theme gray or upgrade gray. After ask_human approval, persist CREATED_WITH_MODERN_THEME=y and APP_THEME_MODE=modern, then read both back. Report partial/unknown writes on failure. Success verifies configuration only, not page migration or runtime rendering. appUpgradeComplete remains false until the Skill verifies all page/form migrations.'],
1180
+ notes: ['--prepare reads remote schemas/source bundles into a new local backup directory and produces a per-resource migration plan without remote writes; it requires explicit upgrade intent but no mutation confirmation. Before --confirm, use the independent upgrade Skill to prepare/review sources, migrate legacy JSX to Canvas without business/layout changes, and remove verified form-detail style injection. Only mutate after an explicit user request to upgrade an existing app to the modern theme AND an affirmative ask_human reply for that app. These flags attest to both checks; they do not replace human confirmation. If ask_human is unavailable, stop before mutation. The CLI itself checks server-derived agentAppType and app/corp identity before offering any confirmation or preparing backups. agentAppType comes from FROM_BUILDER_AI=y plus BUILDER_AI_SOURCE; source alone or FROM_AI does not qualify. Only local/cloud/qwenwork qualifies. Reject non-AI or unverified apps without offering an upgrade or exposing source fields. Skill guidance and confirmation flags cannot bypass this gate. Do not check V4, appThemeEnable, organization theme gray or upgrade gray. After ask_human approval, migrate legacy navigation layout and colors before persisting CREATED_WITH_MODERN_THEME=y and APP_THEME_MODE=modern, then read all changed settings back. Already modern navigation colors stay unchanged. Report partial/unknown writes on failure. Success verifies configuration only, not page migration or runtime rendering. appUpgradeComplete remains false until the Skill verifies all page/form migrations.'],
1177
1181
  }),
1178
1182
  command('app-online', ['app-online'], 'app-online <appType> [--to-ding-app-center] [--show-app-center]', 'help.cmd_app_online'),
1179
1183
  command('app-offline', ['app-offline'], 'app-offline <appType> [--to-ding-app-center] [--show-app-center]', 'help.cmd_app_offline'),
@@ -1889,7 +1893,7 @@ function summarizeLocalizedCommands(commands) {
1889
1893
  plan_command_ids: ['design-plan.catalog', 'design-plan.init', 'design-plan.preview', 'design-plan.materialize', 'design-plan.patch'],
1890
1894
  theme_command_ids: ['sample', 'create-app', 'update-app'],
1891
1895
  navigation_command_ids: { platform: ['update-app', 'nav-group'], custom: ['update-app', 'update-form-config', 'get-form-config'] },
1892
- navigation_policy: 'Before PRD planning in Fast and Plan, the agent determines navigation ownership and layout from business context. Follow yida-design/references/navigation-decision.md for entry and layout decisions. Preserve explicit user requirements and existing navigation for local changes; resource-only requests operate on their specified resources and settings. Use native navigation by default for the application workspace, and custom navigation only for an explicitly justified application-level branded menu or special navigation interaction. A service frontend defaults to one coding page with its own menu and all selected business views; a single-step entry can use navigation.type=none. Choose backend native or coding pages by task efficiency. Backend coding pages in platform-shell are content-only: platform menus own cross-module navigation; local Tabs cover categories or states of the current task only. Never turn the management entry menu into a second page navbar, sidebar or module Tabs. Persistent filters/state alone do not justify custom application navigation. Plan handoff pages include derived navigationPolicy; Fast records the same boundary. Do not hide platform navigation to repair duplicate page menus. Store application navigation in navigationType and standalone entry menus in pageSpecHandoff.navigation. Configure a custom frontend menu at page scope and preserve workspace navigation. Record type/source/reason/variant using ai_default for agent ownership decisions and user_selected for explicit user requirements. Include navigation in the overall Plan confirmation; Fast implements the recorded decision. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, persisting layoutDirection and hideAppNav=n. Preserve stored navType. Without --layout, normalize legacy layoutDirection and navType using the application layout compatibility rules (hoz plus top_side means l_shape; missing layout uses top_fold/top_side for top/l_shape). Apply shell top_fold/none overrides at page scope. Application-wide custom side/top/mixed/dock uses app hiding plus per-page hiding within scope; a standalone frontend in a platform-navigation app uses only page configuration. Read back layoutDirection, hideAppNav and each configured page, then verify the planned frontend and workspace links. Navigation structure is separate from tone. Navigation tone is derived from the final selected theme template, never guessed from the business brief or implicitly defaulted to light; apply that explicit value with update-app --nav-theme. Custom top navigation defaults to edge-to-edge, not floating. Over a hero image, start transparent, add a surface on scroll, restore transparency at the top; expanded menus stay readable. Follow yida-design/references/page-continuity.md; floating requires an explicit design.',
1896
+ navigation_policy: 'Before PRD planning in Fast and Plan, the agent determines navigation ownership and layout from business context. Follow yida-design/references/navigation-decision.md for entry and layout decisions. Preserve explicit user requirements and existing navigation for local changes; resource-only requests operate on their specified resources and settings. Use native navigation by default for the application workspace, and custom navigation only for an explicitly justified application-level branded menu or special navigation interaction. A service frontend defaults to one coding page with its own menu and all selected business views; a single-step entry can use navigation.type=none. Choose backend native or coding pages by task efficiency. Backend coding pages in platform-shell are content-only: platform menus own cross-module navigation; local Tabs cover categories or states of the current task only. Never turn the management entry menu into a second page navbar, sidebar or module Tabs. Persistent filters/state alone do not justify custom application navigation. Plan handoff pages include derived navigationPolicy; Fast records the same boundary. Do not hide platform navigation to repair duplicate page menus. Store application navigation in navigationType and standalone entry menus in pageSpecHandoff.navigation. Configure a custom frontend menu at page scope and preserve workspace navigation. Record type/source/reason/variant using ai_default for agent ownership decisions and user_selected for explicit user requirements. Include navigation in the overall Plan confirmation; Fast implements the recorded decision. Platform navigation uses update-app --layout top|side|l_shape --show-app-nav, persisting layoutDirection and hideAppNav=n. Preserve stored navType. For appThemeEnable=n or appThemeMode=legacy, persist side/top/l_shape as ver/hoz/ver and white/gray as light; without --layout preserve stored slide/ver/hoz. For modern or unknown capability, normalize layoutDirection and navType using the existing application layout compatibility rules (hoz plus top_side means l_shape; missing layout uses top_fold/top_side for top/l_shape). Apply shell top_fold/none overrides at page scope. Application-wide custom side/top/mixed/dock uses app hiding plus per-page hiding within scope; a standalone frontend in a platform-navigation app uses only page configuration. Read back layoutDirection, hideAppNav and each configured page, then verify the planned frontend and workspace links. Navigation structure is separate from tone. Navigation tone is derived from the final selected theme template, never guessed from the business brief or implicitly defaulted to light; apply that explicit value with update-app --nav-theme. Custom top navigation defaults to edge-to-edge, not floating. Over a hero image, start transparent, add a surface on scroll, restore transparency at the top; expanded menus stay readable. Follow yida-design/references/page-continuity.md; floating requires an explicit design.',
1893
1897
  design_mode_policy: 'Analyze requirements first with yida-requirement-analysis: read only necessary sources before the first question, offer concrete core-function candidates, clarify key usage, and preserve explicit scope and valid answers. Initial intake focuses on core functions and actual usage. Record actual user replies; proceed with dependent planning after a reply or explicit delegation. First-time builds include new apps and existing apps without business pages. After intake, reuse explicit mode choices and detailed supplied plans; if the user delegates the mode or asks for no more questions, use Fast unless Plan is explicit; otherwise, if the mode is unspecified, ask whether to prepare a PRD for confirmation (Plan, more detailed and slower) or build from the supplied requirements (Fast). Without detailed requirements, offer Fast and Plan neutrally. intake.confirmed records scope and key-usage clarification; the plan revision tracks plan approval separately. Fast and Plan share one three-direction visual generation rule. Fast compares exactly three directions internally and keeps the selected result. When Plan is explicit and the user has not supplied a complete visual direction, generate exactly three directions and use ask_human for the style choice before design-plan init; do not wait for the user to request a comparison. Planning preparation derives page scenes, selected theme mapping and navigation structure before init; mark AI suggestions and apply navigation_policy. Existing business apps only clarify the current change. A named resource-only request with explicitScope.allowInferredResources=false keeps planning within the specified resources. When theme mapping is missing, query design-plan catalog once and reuse its themes and pagePatterns. Init returns authoring.pendingFields and context with typed examples; complete pending facts and review coverage before setting ready=true. Reuse one complete agent-capabilities summary per unchanged environment. Standard Plan uses design-plan init parallelTasks, then executes the returned materialize.command exactly once with business-file and visual-file; preview and --from-preview are only for explicitly incremental large plans. Present the current plan without revision numbers in user-visible titles, summaries, attachments or questions. Keep revision in internal confirmation fields, validate it before building, and execute its generated artifacts.',
1894
1898
  product_design_policy: 'yida-requirement-analysis owns shared facts and first-time intake. yida-prd owns business planning; yida-design owns visual design. Reuse the same confirmed brief and supplied details. Prepare business and base visuals concurrently, then bind visuals to settled page tasks. yida-app merges and checks the artifacts before creating resources. Plan authors scope, data and rules, page organization, business interactions and acceptance once. Ordered blocks {name,purpose} supply default priority and content layers; explicit designs remain authoritative. Project-specific acceptance criteria are merged with generated resource checks. Plan uses the compact authoring contract and selected theme context; the CLI reads full templates and renders all artifacts. Fast, Plan and single-page design share yida-design/templates/design-themes and the same three-direction generation rule. Theme templates declare application-global variables from basic-tokens.json with fixed typography, spacing and Tooltip colors, plus scoped custom-page variables; component recipes consume those variables. Each selected template is the authority for navTheme, its six navigation color tokens, and its selected-item shadow token; free-creative authors its own navigation tone and tokens. Explicit project customization stays in visualStyle.tokens. The CLI declares each fixed application palette once in a shared :root and platform-mode selector block. The CLI preserves mode-independent navigation appearance tokens. Light, dark, white and gray mode classes consume the same project tokens; navTheme remains platform metadata, not a second palette. Legacy generated mode blocks are consolidated on update, while unrelated custom CSS is preserved.',
1895
1899
  ui_guidance_policy: 'Page implementation consumes yida-prd prd.md for positioning, information architecture, page prototype, native form entry policy, material strategy, business-specific checks, resource creation order, page implementation delivery order, navigation order, and acceptance criteria; it consumes yida-design design.md for app custom theme CSS delivery, themeColor/navTheme, visual states, visualScaffold, surface material, rounded rules, density rules, components, state styling, and page-level imageNeed. When a page is imageNeed=required, or beneficial with declared image slots, yida-image-assets prepares a traceable asset manifest alongside page layout and interaction work; wait for final images only at that page\'s image binding and acceptance. imageNeed=none skips the branch. prd.md and design.md remain the only design sources of truth. Page implementation may extract page-spec.json as a derived implementation handoff from prd.md + design.md; conflicts are resolved by sending business conflicts to yida-prd and visual conflicts to yida-design before regenerating the spec. Core normal forms default to 1-3 business sample records before page implementation, followed by query readback. An explicit opt-out, configuration dictionary, sensitive data, or lack of safely constructible values requires a recorded skip reason. Screenshots, public sharing, data-source deep binding, and fine navigation grouping are optional after explicit user request or PRD acceptance criteria.',
@@ -1175,15 +1175,19 @@ Examples:
1175
1175
 
1176
1176
  // ── lib/update-app.js ─────────────────────────────
1177
1177
  update_app: {
1178
+ legacy_style_confirm_required: 'Application CSS is discouraged for legacy or unverified themes. Review CSS scope and ask the human to confirm injection into this exact application (navigation, forms and visit pages may be affected). Only then pass --confirm-legacy-app-style. Otherwise use page-scoped styles.',
1179
+ disable_theme_conflict: '--disable-custom-theme only disables CSS and cannot be combined with other application updates.',
1180
+ custom_theme_config_invalid: 'customThemeStyle is missing, malformed, or lacks a nonempty cssUrl. No disable request was submitted; inspect the latest app configuration.',
1181
+ custom_theme_disabled: 'Custom theme CSS is disabled and resource settings are retained; runtime page verification remains pending.',
1178
1182
  theme_preset_conflict: 'A preset colour cannot be combined with custom CSS or themeColor. Use --colour custom or omit --colour.',
1179
1183
  custom_theme_color_required: 'colour=custom requires a theme file or a valid themeColor. Pass --theme-file or --theme-color.',
1180
- theme_not_persisted: 'App theme readback failed or did not match the saved settings. The CSS resource binding is unverified. Inspect themeVerification and retry update-app <appType> --theme-file <css>; do not refresh the app.',
1184
+ theme_not_persisted: 'App theme readback failed or did not match the saved settings. CSS binding or removal is unverified. Inspect expected/actual values and readback errors in themeVerification, then check current app settings before repeating an upload or recreating the app.',
1181
1185
  navigation_not_persisted: 'App navigation readback failed or did not match the requested settings. Navigation changes remain unverified. Inspect the expected values, actual values and readback error in navigationVerification, then check the current app settings.',
1182
- usage: 'openyida update-app <appType> [--name "New Name"] [--theme-file ./theme.css] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape]',
1186
+ usage: 'openyida update-app <appType> [--name "New Name"] [--theme-file ./theme.css] [--confirm-legacy-app-style] [--disable-custom-theme] [--nav-theme light|dark|white|gray] [--logo-source appIcon|customImage] [--layout side|top|l_shape]',
1183
1187
  example: 'openyida update-app APP_XXX --theme-file ./app-theme.css --nav-theme light --logo-source appIcon --layout side',
1184
- options: 'Options:\n --name, -n App name (specify at least one update field)\n --desc, -d App description\n --icon Icon name (e.g. xian-yingyong)\n --icon-color Icon color (default: #0089FF)\n --colour, --theme podBlue | podGreen | podOrange | deepBlue | teal | black | custom ...\n --theme-color Update the app primary color; the CSS primary color takes precedence when a theme file is supplied\n --theme-file Custom app theme CSS file (up to 20 MB; must declare --color-brand1-6)\n --nav-theme Navigation theme: light, dark, white, or gray\n --logo-source Navigation logo source: appIcon or customImage\n --layout Modern navigation layout: side, top, or l_shape\n --hide-app-nav Hide app navigation in visit mode, only for custom pages with self-rendered top/side navigation\n --show-app-nav Restore app navigation in visit mode',
1188
+ options: 'Options:\n --name, -n App name (specify at least one update field)\n --desc, -d App description\n --icon Icon name (e.g. xian-yingyong)\n --icon-color Icon color (default: #0089FF)\n --colour, --theme podBlue | podGreen | podOrange | deepBlue | teal | black | custom ...\n --theme-color Update the app primary color; the CSS primary color takes precedence when a theme file is supplied\n --theme-file Custom app theme CSS file (up to 20 MB; must declare --color-brand1-6)\n --nav-theme Navigation theme: light, dark, white, or gray\n --logo-source Navigation logo source: appIcon or customImage\n --layout Modern navigation layout: side, top, or l_shape\n --hide-app-nav Hide app navigation in visit mode, only for custom pages with self-rendered top/side navigation\n --show-app-nav Restore app navigation in visit mode\n --confirm-legacy-app-style Allow legacy app CSS only after human approval\n --disable-custom-theme Disable application CSS, retain resource URL and primary color',
1185
1189
  missing_app_type: 'Error: missing appType argument',
1186
- missing_update_field: 'Error: at least one update field must be specified (--name, --desc, --icon, --theme, --theme-color, --theme-file, --nav-theme, --logo-source, --layout, --hide-app-nav, --show-app-nav)',
1190
+ missing_update_field: 'Error: at least one update field must be specified (--name, --desc, --icon, --theme, --theme-color, --theme-file, --nav-theme, --logo-source, --layout, --hide-app-nav, --show-app-nav, --disable-custom-theme)',
1187
1191
  title: ' update-app - Yida App Info Update Tool',
1188
1192
  app_type: '\n App ID: {0}',
1189
1193
  new_name: ' New name: {0}',
@@ -2169,7 +2173,7 @@ module.exports.upgrade_app_theme = {
2169
2173
  prepare_blocked: 'Some schemas or sources could not be retrieved or require manual migration. Inspect upgrade-plan.json; remote resources were not modified.',
2170
2174
 
2171
2175
  context_failed: 'Cannot verify the server context for this app and organization. Upgrade stopped.',
2172
- ai_only: 'This app does not currently support upgrading to the new theme.',
2176
+ ai_only: 'This operation is unavailable for this app.',
2173
2177
  usage: 'openyida upgrade-app-theme <appType> --explicit-request [--confirm | --prepare --output-dir <dir>] [--json]',
2174
2178
  confirm_required: 'Requires an explicit upgrade request and ask_human approval of the upgrade scope. Both --explicit-request and --confirm are required.',
2175
2179
  read_failed: 'Cannot read theme upgrade configuration. Complete upgrade status is unverified.',