kintone-migrator 0.34.3 → 0.34.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -10428,6 +10428,128 @@ const CustomizationConfigSerializer = { serialize: (config) => {
10428
10428
  };
10429
10429
  } };
10430
10430
  //#endregion
10431
+ //#region src/core/domain/customization/services/customizationMerge.ts
10432
+ const PLATFORMS = ["desktop", "mobile"];
10433
+ const CATEGORIES = ["js", "css"];
10434
+ const SCOPE_KEY = "config:scope";
10435
+ /**
10436
+ * Bucket-qualified resource identity: `platform:category:name`. Shared with the
10437
+ * force/firstTime pull path (`preservePaths`) so path preservation matches the
10438
+ * merge on the same identity and cross-bucket same-basename files stay distinct.
10439
+ */
10440
+ function resourceKey(platform, category, name) {
10441
+ return `${platform}:${category}:${name}`;
10442
+ }
10443
+ function platformList(config, platform, category) {
10444
+ const p = config[platform];
10445
+ return category === "js" ? p.js : p.css;
10446
+ }
10447
+ /**
10448
+ * Builds the per-side map keyed by `platform:category:name`. `contentTag`
10449
+ * returns the content marker for a same-name FILE so divergent content surfaces
10450
+ * as a conflict. `side` distinguishes local vs remote tags for modified files.
10451
+ */
10452
+ function toMap(config, contentTag) {
10453
+ const map = /* @__PURE__ */ new Map();
10454
+ for (const platform of PLATFORMS) for (const category of CATEGORIES) for (const resource of platformList(config, platform, category)) {
10455
+ const name = resourceName(resource);
10456
+ const tag = resource.type === "FILE" ? `FILE:${contentTag(name)}` : "URL";
10457
+ map.set(resourceKey(platform, category, name), {
10458
+ resource,
10459
+ tag
10460
+ });
10461
+ }
10462
+ const scope = config.scope ?? "ALL";
10463
+ map.set(SCOPE_KEY, { scope });
10464
+ return map;
10465
+ }
10466
+ function isScopeValue(v) {
10467
+ return "scope" in v;
10468
+ }
10469
+ function eq(a, b) {
10470
+ if (isScopeValue(a) || isScopeValue(b)) return isScopeValue(a) && isScopeValue(b) && a.scope === b.scope;
10471
+ return a.tag === b.tag;
10472
+ }
10473
+ /**
10474
+ * Computes the file-name-keyed 3-way merge of base/local/remote customization.
10475
+ *
10476
+ * `modifiedFileNames` is the set of same-name FILE basenames whose local
10477
+ * content differs from the remote content (reused from the diffDetector flow).
10478
+ * Such files get distinct local/remote tags so they classify as conflicts; all
10479
+ * other matched files share the `"same"` tag and classify as unchanged.
10480
+ */
10481
+ function computeCustomizationThreeWayMerge(base, local, remote, modifiedFileNames) {
10482
+ const tagFor = (side) => (name) => modifiedFileNames.has(name) ? side : "same";
10483
+ return classifyThreeWay(toMap(base, () => "same"), toMap(local, tagFor("local")), toMap(remote, tagFor("remote")), eq);
10484
+ }
10485
+ /**
10486
+ * Applies a resolved 3-way merge, returning the merged customization config.
10487
+ *
10488
+ * Auto-merged entries take their `merged` value; conflicts take the chosen
10489
+ * side. The resulting resources are flattened back into per-platform js/css
10490
+ * lists, preserving the order they appear in the chosen/merged side. Throws a
10491
+ * BusinessRuleError when a conflict is left unresolved (programmer invariant).
10492
+ */
10493
+ function resolveCustomizationMerge(merge, resolution, local, remote) {
10494
+ const chosen = /* @__PURE__ */ new Map();
10495
+ let scope = "ALL";
10496
+ for (const entry of merge.entries) {
10497
+ let value;
10498
+ if (entry.change.kind === "conflict") {
10499
+ const choice = resolution.get(entry.key);
10500
+ if (choice === void 0) throw new BusinessRuleError(CustomizationErrorCode.CzUnresolvedConflict, `Unresolved conflict for "${entry.key}"`);
10501
+ value = choice === "local" ? entry.local : entry.remote;
10502
+ } else value = entry.merged;
10503
+ if (value === void 0) continue;
10504
+ if (isScopeValue(value)) scope = value.scope;
10505
+ else chosen.set(entry.key, value.resource);
10506
+ }
10507
+ return rebuild(chosen, scope, local, remote);
10508
+ }
10509
+ /**
10510
+ * Rebuilds the config from the chosen resource keys, ordering each bucket by the
10511
+ * resource's position in local first, then any remote-only resources, so the
10512
+ * resulting list is stable and matches the user's local ordering intent.
10513
+ *
10514
+ * `path` is a local-owned concern (the build-output location) that is orthogonal
10515
+ * to which content wins the merge: whenever a key exists locally, the local
10516
+ * resource (its declared path) is kept regardless of the chosen content side.
10517
+ * Only remote-only keys fall back to the chosen (remote/basename) resource.
10518
+ */
10519
+ function rebuild(chosen, scope, local, remote) {
10520
+ function bucket(platform, category) {
10521
+ const seen = /* @__PURE__ */ new Set();
10522
+ const out = [];
10523
+ const localByKey = /* @__PURE__ */ new Map();
10524
+ for (const resource of platformList(local, platform, category)) localByKey.set(resourceKey(platform, category, resourceName(resource)), resource);
10525
+ const append = (resources) => {
10526
+ for (const resource of resources) {
10527
+ const key = resourceKey(platform, category, resourceName(resource));
10528
+ if (seen.has(key)) continue;
10529
+ const picked = chosen.get(key);
10530
+ if (picked !== void 0) {
10531
+ out.push(localByKey.get(key) ?? picked);
10532
+ seen.add(key);
10533
+ }
10534
+ }
10535
+ };
10536
+ append(platformList(local, platform, category));
10537
+ append(platformList(remote, platform, category));
10538
+ return out;
10539
+ }
10540
+ return {
10541
+ scope,
10542
+ desktop: {
10543
+ js: bucket("desktop", "js"),
10544
+ css: bucket("desktop", "css")
10545
+ },
10546
+ mobile: {
10547
+ js: bucket("mobile", "js"),
10548
+ css: bucket("mobile", "css")
10549
+ }
10550
+ };
10551
+ }
10552
+ //#endregion
10431
10553
  //#region src/lib/deduplicateName.ts
10432
10554
  const DEFAULT_MAX_COUNTER = 1e4;
10433
10555
  /**
@@ -10500,7 +10622,7 @@ function usedStemsFor(usedNames, ext) {
10500
10622
  for (const name of usedNames) if (name.endsWith(ext)) stems.add(name.slice(0, name.length - ext.length));
10501
10623
  return stems;
10502
10624
  }
10503
- function planResources(resources, platformDir, resourceType, relativeBaseDir) {
10625
+ function planResources(resources, platformName, platformDir, resourceType, relativeBaseDir, contentBase, preservePaths) {
10504
10626
  const planned = [];
10505
10627
  const filesToDownload = [];
10506
10628
  const dir = join(platformDir, resourceType);
@@ -10511,28 +10633,42 @@ function planResources(resources, platformDir, resourceType, relativeBaseDir) {
10511
10633
  });
10512
10634
  else {
10513
10635
  const fileName = deduplicateFileName(sanitizeFileName(resource.file.name), usedNames);
10514
- const absolutePath = join(dir, fileName);
10515
- const relativePath = join(relativeBaseDir, resourceType, fileName);
10516
- planned.push({
10517
- type: "FILE",
10518
- path: relativePath
10519
- });
10520
- filesToDownload.push({
10521
- fileName,
10522
- fileKey: resource.file.fileKey,
10523
- absolutePath
10524
- });
10636
+ const declaredPath = preservePaths?.get(resourceKey(platformName, resourceType, resource.file.name));
10637
+ if (declaredPath !== void 0) {
10638
+ planned.push({
10639
+ type: "FILE",
10640
+ path: declaredPath
10641
+ });
10642
+ filesToDownload.push({
10643
+ fileName,
10644
+ fileKey: resource.file.fileKey,
10645
+ absolutePath: join(contentBase, declaredPath)
10646
+ });
10647
+ } else {
10648
+ const absolutePath = join(dir, fileName);
10649
+ const relativePath = join(relativeBaseDir, resourceType, fileName);
10650
+ planned.push({
10651
+ type: "FILE",
10652
+ path: relativePath
10653
+ });
10654
+ filesToDownload.push({
10655
+ fileName,
10656
+ fileKey: resource.file.fileKey,
10657
+ absolutePath
10658
+ });
10659
+ }
10525
10660
  }
10526
10661
  return {
10527
10662
  resources: planned,
10528
10663
  filesToDownload
10529
10664
  };
10530
10665
  }
10531
- function planPlatform(remotePlatform, platformName, basePath, filePrefix) {
10532
- const platformDir = join(basePath, filePrefix, platformName);
10666
+ function planPlatform(remotePlatform, platformName, basePath, filePrefix, preservePaths) {
10667
+ const contentBase = join(basePath, filePrefix);
10668
+ const platformDir = join(contentBase, platformName);
10533
10669
  const platformPrefix = platformName;
10534
- const jsPlan = planResources(remotePlatform.js, platformDir, "js", platformPrefix);
10535
- const cssPlan = planResources(remotePlatform.css, platformDir, "css", platformPrefix);
10670
+ const jsPlan = planResources(remotePlatform.js, platformName, platformDir, "js", platformPrefix, contentBase, preservePaths);
10671
+ const cssPlan = planResources(remotePlatform.css, platformName, platformDir, "css", platformPrefix, contentBase, preservePaths);
10536
10672
  const fileCount = remotePlatform.js.filter((r) => r.type === "FILE").length + remotePlatform.css.filter((r) => r.type === "FILE").length;
10537
10673
  return {
10538
10674
  platform: {
@@ -10560,8 +10696,8 @@ async function downloadFiles(files, container) {
10560
10696
  async function captureCustomization({ container, input }) {
10561
10697
  const existing = await container.customizationStorage.get();
10562
10698
  const { scope, desktop, mobile } = await container.customizationConfigurator.getCustomization();
10563
- const desktopPlan = planPlatform(desktop, "desktop", input.basePath, input.filePrefix);
10564
- const mobilePlan = planPlatform(mobile, "mobile", input.basePath, input.filePrefix);
10699
+ const desktopPlan = planPlatform(desktop, "desktop", input.basePath, input.filePrefix, input.preservePaths);
10700
+ const mobilePlan = planPlatform(mobile, "mobile", input.basePath, input.filePrefix, input.preservePaths);
10565
10701
  const config = {
10566
10702
  scope,
10567
10703
  desktop: desktopPlan.platform,
@@ -10571,6 +10707,7 @@ async function captureCustomization({ container, input }) {
10571
10707
  await downloadFiles([...desktopPlan.filesToDownload, ...mobilePlan.filesToDownload], container);
10572
10708
  return {
10573
10709
  configText,
10710
+ config,
10574
10711
  hasExistingConfig: existing.exists,
10575
10712
  fileResourceCount: desktopPlan.fileCount + mobilePlan.fileCount
10576
10713
  };
@@ -11565,116 +11702,6 @@ var apply_default$9 = define({
11565
11702
  }
11566
11703
  });
11567
11704
  //#endregion
11568
- //#region src/core/domain/customization/services/customizationMerge.ts
11569
- const PLATFORMS = ["desktop", "mobile"];
11570
- const CATEGORIES = ["js", "css"];
11571
- const SCOPE_KEY = "config:scope";
11572
- function resourceKey(platform, category, name) {
11573
- return `${platform}:${category}:${name}`;
11574
- }
11575
- function platformList(config, platform, category) {
11576
- const p = config[platform];
11577
- return category === "js" ? p.js : p.css;
11578
- }
11579
- /**
11580
- * Builds the per-side map keyed by `platform:category:name`. `contentTag`
11581
- * returns the content marker for a same-name FILE so divergent content surfaces
11582
- * as a conflict. `side` distinguishes local vs remote tags for modified files.
11583
- */
11584
- function toMap(config, contentTag) {
11585
- const map = /* @__PURE__ */ new Map();
11586
- for (const platform of PLATFORMS) for (const category of CATEGORIES) for (const resource of platformList(config, platform, category)) {
11587
- const name = resourceName(resource);
11588
- const tag = resource.type === "FILE" ? `FILE:${contentTag(name)}` : "URL";
11589
- map.set(resourceKey(platform, category, name), {
11590
- resource,
11591
- tag
11592
- });
11593
- }
11594
- const scope = config.scope ?? "ALL";
11595
- map.set(SCOPE_KEY, { scope });
11596
- return map;
11597
- }
11598
- function isScopeValue(v) {
11599
- return "scope" in v;
11600
- }
11601
- function eq(a, b) {
11602
- if (isScopeValue(a) || isScopeValue(b)) return isScopeValue(a) && isScopeValue(b) && a.scope === b.scope;
11603
- return a.tag === b.tag;
11604
- }
11605
- /**
11606
- * Computes the file-name-keyed 3-way merge of base/local/remote customization.
11607
- *
11608
- * `modifiedFileNames` is the set of same-name FILE basenames whose local
11609
- * content differs from the remote content (reused from the diffDetector flow).
11610
- * Such files get distinct local/remote tags so they classify as conflicts; all
11611
- * other matched files share the `"same"` tag and classify as unchanged.
11612
- */
11613
- function computeCustomizationThreeWayMerge(base, local, remote, modifiedFileNames) {
11614
- const tagFor = (side) => (name) => modifiedFileNames.has(name) ? side : "same";
11615
- return classifyThreeWay(toMap(base, () => "same"), toMap(local, tagFor("local")), toMap(remote, tagFor("remote")), eq);
11616
- }
11617
- /**
11618
- * Applies a resolved 3-way merge, returning the merged customization config.
11619
- *
11620
- * Auto-merged entries take their `merged` value; conflicts take the chosen
11621
- * side. The resulting resources are flattened back into per-platform js/css
11622
- * lists, preserving the order they appear in the chosen/merged side. Throws a
11623
- * BusinessRuleError when a conflict is left unresolved (programmer invariant).
11624
- */
11625
- function resolveCustomizationMerge(merge, resolution, local, remote) {
11626
- const chosen = /* @__PURE__ */ new Map();
11627
- let scope = "ALL";
11628
- for (const entry of merge.entries) {
11629
- let value;
11630
- if (entry.change.kind === "conflict") {
11631
- const choice = resolution.get(entry.key);
11632
- if (choice === void 0) throw new BusinessRuleError(CustomizationErrorCode.CzUnresolvedConflict, `Unresolved conflict for "${entry.key}"`);
11633
- value = choice === "local" ? entry.local : entry.remote;
11634
- } else value = entry.merged;
11635
- if (value === void 0) continue;
11636
- if (isScopeValue(value)) scope = value.scope;
11637
- else chosen.set(entry.key, value.resource);
11638
- }
11639
- return rebuild(chosen, scope, local, remote);
11640
- }
11641
- /**
11642
- * Rebuilds the config from the chosen resource keys, ordering each bucket by the
11643
- * resource's position in local first, then any remote-only resources, so the
11644
- * resulting list is stable and matches the user's local ordering intent.
11645
- */
11646
- function rebuild(chosen, scope, local, remote) {
11647
- function bucket(platform, category) {
11648
- const seen = /* @__PURE__ */ new Set();
11649
- const out = [];
11650
- const append = (resources) => {
11651
- for (const resource of resources) {
11652
- const key = resourceKey(platform, category, resourceName(resource));
11653
- if (seen.has(key)) continue;
11654
- const picked = chosen.get(key);
11655
- if (picked !== void 0) {
11656
- out.push(picked);
11657
- seen.add(key);
11658
- }
11659
- }
11660
- };
11661
- append(platformList(local, platform, category));
11662
- append(platformList(remote, platform, category));
11663
- return out;
11664
- }
11665
- return {
11666
- scope,
11667
- desktop: {
11668
- js: bucket("desktop", "js"),
11669
- css: bucket("desktop", "css")
11670
- },
11671
- mobile: {
11672
- js: bucket("mobile", "js"),
11673
- css: bucket("mobile", "css")
11674
- }
11675
- };
11676
- }
11677
- //#endregion
11678
11705
  //#region src/core/application/customization/customizationRemote.ts
11679
11706
  /**
11680
11707
  * Converts the kintone remote customization (file metadata) into a
@@ -11899,15 +11926,17 @@ async function saveCustomizationSnapshotAndRevision(container, config, revision)
11899
11926
  async function pullCustomization({ container, input }) {
11900
11927
  const { state, local, remote } = await loadCustomizationThreeWayInputs(container);
11901
11928
  if (input.force || state === void 0 || local === void 0) {
11929
+ const preservePaths = local !== void 0 ? buildPreservePaths(local) : void 0;
11902
11930
  const captured = await captureCustomization({
11903
11931
  container,
11904
11932
  input: {
11905
11933
  basePath: input.captureBasePath,
11906
- filePrefix: input.filePrefix
11934
+ filePrefix: input.filePrefix,
11935
+ preservePaths
11907
11936
  }
11908
11937
  });
11909
11938
  await container.customizationStorage.update(captured.configText);
11910
- await saveCustomizationSnapshotAndRevision(container, remote.config, remote.revision);
11939
+ await saveCustomizationSnapshotAndRevision(container, captured.config, remote.revision);
11911
11940
  return { mode: input.force ? "force" : "firstTime" };
11912
11941
  }
11913
11942
  const modifiedFileNames = await computeModifiedFileNames(local, remote.raw, input.basePath, container);
@@ -11955,7 +11984,26 @@ async function applyPulledCustomizationMerge({ container, input }) {
11955
11984
  await Promise.all(downloads);
11956
11985
  const configText = stringifyConfig(container.configCodec, CustomizationConfigSerializer.serialize(merged));
11957
11986
  await container.customizationStorage.update(configText);
11958
- await saveCustomizationSnapshotAndRevision(container, input.remoteConfig, input.remoteRevision);
11987
+ await saveCustomizationSnapshotAndRevision(container, merged, input.remoteRevision);
11988
+ }
11989
+ /**
11990
+ * Builds the path-preservation map for `captureCustomization`: bucket-qualified
11991
+ * key (`resourceKey(platform, category, FILE basename)`) → declared local
11992
+ * relative path. The composite key matches the merge identity so cross-bucket
11993
+ * same-basename files (e.g. `app/desktop/js/main.js` vs `app/mobile/js/main.js`)
11994
+ * stay distinct instead of colliding under a flat basename map. Only FILE
11995
+ * resources carry a path.
11996
+ */
11997
+ function buildPreservePaths(config) {
11998
+ const map = /* @__PURE__ */ new Map();
11999
+ const add = (platform, category, resources) => {
12000
+ for (const resource of resources) if (resource.type === "FILE") map.set(resourceKey(platform, category, resourceName(resource)), resource.path);
12001
+ };
12002
+ add("desktop", "js", config.desktop.js);
12003
+ add("desktop", "css", config.desktop.css);
12004
+ add("mobile", "js", config.mobile.js);
12005
+ add("mobile", "css", config.mobile.css);
12006
+ return map;
11959
12007
  }
11960
12008
  function allFileResources(config) {
11961
12009
  return [
@@ -11987,6 +12035,25 @@ function remoteFileKeyMap(remote) {
11987
12035
  }
11988
12036
  //#endregion
11989
12037
  //#region src/cli/commands/customize/pull.ts
12038
+ /**
12039
+ * Derives the path bases `runPull` feeds to the application layer.
12040
+ *
12041
+ * - `basePath` (= push's {@link computeBasePath}) is where merged/downloaded
12042
+ * content resolves and push uploads from.
12043
+ * - `captureBasePath` + `filePrefix` are passed to the capture-equivalent
12044
+ * (force / first-time) path.
12045
+ *
12046
+ * Invariant: `join(captureBasePath, filePrefix) === basePath`, so pull writes
12047
+ * exactly where push uploads from. Exported so the invariant test pins these
12048
+ * exact derivations rather than a hand-mirrored copy.
12049
+ */
12050
+ function derivePullPaths(customizeFilePath) {
12051
+ return {
12052
+ basePath: computeBasePath(customizeFilePath),
12053
+ captureBasePath: dirname(resolve(customizeFilePath)),
12054
+ filePrefix: deriveFilePrefix(customizeFilePath)
12055
+ };
12056
+ }
11990
12057
  async function resolveConflicts$1(merged, options) {
11991
12058
  const resolution = /* @__PURE__ */ new Map();
11992
12059
  for (const conflict of merged.merge.conflicts) {
@@ -12015,9 +12082,7 @@ async function resolveConflicts$1(merged, options) {
12015
12082
  }
12016
12083
  async function runPull$1(containerConfig, options) {
12017
12084
  const container = createCustomizationCliContainer(containerConfig);
12018
- const basePath = computeBasePath(containerConfig.customizeFilePath);
12019
- const captureBasePath = dirname(resolve(containerConfig.customizeFilePath));
12020
- const filePrefix = deriveFilePrefix(containerConfig.customizeFilePath);
12085
+ const { basePath, captureBasePath, filePrefix } = derivePullPaths(containerConfig.customizeFilePath);
12021
12086
  const s = p.spinner();
12022
12087
  s.start("Pulling customization from kintone...");
12023
12088
  let result;
@@ -16275,6 +16340,7 @@ function buildPhases(args) {
16275
16340
  container: c.schema,
16276
16341
  input: { force }
16277
16342
  });
16343
+ return [];
16278
16344
  }
16279
16345
  }]
16280
16346
  },
@@ -16291,6 +16357,7 @@ function buildPhases(args) {
16291
16357
  force
16292
16358
  }
16293
16359
  });
16360
+ return [];
16294
16361
  }
16295
16362
  }, {
16296
16363
  domain: "view",
@@ -16300,6 +16367,7 @@ function buildPhases(args) {
16300
16367
  container: c.view,
16301
16368
  input: { force }
16302
16369
  });
16370
+ return [];
16303
16371
  }
16304
16372
  }]
16305
16373
  },
@@ -16314,6 +16382,7 @@ function buildPhases(args) {
16314
16382
  container: c.fieldPermission,
16315
16383
  input: { force }
16316
16384
  });
16385
+ return [];
16317
16386
  }
16318
16387
  },
16319
16388
  {
@@ -16324,6 +16393,7 @@ function buildPhases(args) {
16324
16393
  container: c.appPermission,
16325
16394
  input: { force }
16326
16395
  });
16396
+ return [];
16327
16397
  }
16328
16398
  },
16329
16399
  {
@@ -16334,6 +16404,7 @@ function buildPhases(args) {
16334
16404
  container: c.recordPermission,
16335
16405
  input: { force }
16336
16406
  });
16407
+ return [];
16337
16408
  }
16338
16409
  }
16339
16410
  ]
@@ -16349,6 +16420,7 @@ function buildPhases(args) {
16349
16420
  container: c.settings,
16350
16421
  input: { force }
16351
16422
  });
16423
+ return [];
16352
16424
  }
16353
16425
  },
16354
16426
  {
@@ -16359,6 +16431,7 @@ function buildPhases(args) {
16359
16431
  container: c.notification,
16360
16432
  input: { force }
16361
16433
  });
16434
+ return [];
16362
16435
  }
16363
16436
  },
16364
16437
  {
@@ -16369,6 +16442,7 @@ function buildPhases(args) {
16369
16442
  container: c.report,
16370
16443
  input: { force }
16371
16444
  });
16445
+ return [];
16372
16446
  }
16373
16447
  },
16374
16448
  {
@@ -16379,6 +16453,7 @@ function buildPhases(args) {
16379
16453
  container: c.action,
16380
16454
  input: { force }
16381
16455
  });
16456
+ return [];
16382
16457
  }
16383
16458
  },
16384
16459
  {
@@ -16389,6 +16464,7 @@ function buildPhases(args) {
16389
16464
  container: c.process,
16390
16465
  input: { force }
16391
16466
  });
16467
+ return [];
16392
16468
  }
16393
16469
  },
16394
16470
  {
@@ -16399,16 +16475,34 @@ function buildPhases(args) {
16399
16475
  container: c.adminNotes,
16400
16476
  input: { force }
16401
16477
  });
16478
+ return [];
16402
16479
  }
16403
16480
  },
16404
16481
  {
16405
16482
  domain: "plugin",
16406
16483
  storageExists: async () => (await c.plugin.pluginStorage.get()).exists,
16407
16484
  run: async () => {
16408
- await pushPlugin({
16485
+ const result = await pushPlugin({
16409
16486
  container: c.plugin,
16410
16487
  input: { force }
16411
16488
  });
16489
+ const warnings = [];
16490
+ const deletions = result.skipped.filter((o) => o.reason === "delete").map((o) => o.pluginId);
16491
+ const modifications = result.skipped.filter((o) => o.reason === "modify").map((o) => o.pluginId);
16492
+ const addDisabled = result.skipped.filter((o) => o.reason === "add-disabled").map((o) => o.pluginId);
16493
+ if (deletions.length > 0) warnings.push({
16494
+ domain: "plugin",
16495
+ message: `Cannot remove plugins via the kintone API (add-only); left on the app: ${deletions.join(", ")}`
16496
+ });
16497
+ if (modifications.length > 0) warnings.push({
16498
+ domain: "plugin",
16499
+ message: `Cannot modify existing plugins (name/enabled) via the kintone API (add-only); unchanged: ${modifications.join(", ")}`
16500
+ });
16501
+ if (addDisabled.length > 0) warnings.push({
16502
+ domain: "plugin",
16503
+ message: `Cannot add plugins in a disabled state via the kintone API (add-only; adding would force-enable them); not added — set enabled: false manually in the kintone admin UI: ${addDisabled.join(", ")}`
16504
+ });
16505
+ return warnings;
16412
16506
  }
16413
16507
  }
16414
16508
  ]
@@ -16448,11 +16542,12 @@ async function executeSchemaPhase(phase, containers, state) {
16448
16542
  continue;
16449
16543
  }
16450
16544
  try {
16451
- await task.run();
16545
+ const warnings = await task.run();
16452
16546
  await deployApp({ container: containers.schema });
16453
16547
  results.push({
16454
16548
  domain: task.domain,
16455
- success: true
16549
+ success: true,
16550
+ warnings
16456
16551
  });
16457
16552
  } catch (error) {
16458
16553
  const err = toError(error);
@@ -16499,10 +16594,11 @@ async function executeStandardPhase(phase, state) {
16499
16594
  continue;
16500
16595
  }
16501
16596
  try {
16502
- await task.run();
16597
+ const warnings = await task.run();
16503
16598
  results.push({
16504
16599
  domain: task.domain,
16505
- success: true
16600
+ success: true,
16601
+ warnings
16506
16602
  });
16507
16603
  } catch (error) {
16508
16604
  const err = toError(error);
@@ -16615,6 +16711,7 @@ function printPhaseResult(phaseResult) {
16615
16711
  for (const result of phaseResult.results) {
16616
16712
  p.log.message(formatTaskResult(result));
16617
16713
  if (!result.success && result.skipped === false) logError(result.error);
16714
+ if (result.success) for (const warning of result.warnings) p.log.warn(warning.message);
16618
16715
  }
16619
16716
  }
16620
16717
  function printPushAllResults(output) {