kintone-migrator 0.24.4 → 0.24.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
@@ -4,9 +4,10 @@ import "dotenv/config";
4
4
  import { cli, define } from "gunshi";
5
5
  import * as p from "@clack/prompts";
6
6
  import { parse, stringify } from "yaml";
7
+ import { KintoneRestAPIClient, KintoneRestAPIError } from "@kintone/rest-api-client";
7
8
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
8
9
  import { basename, dirname, extname, join, resolve } from "node:path";
9
- import { KintoneRestAPIClient, KintoneRestAPIError } from "@kintone/rest-api-client";
10
+ import { realpathSync } from "node:fs";
10
11
  import * as v from "valibot";
11
12
  import pc from "picocolors";
12
13
 
@@ -290,6 +291,10 @@ var ConflictError = class extends ApplicationError {
290
291
  function isConflictError(error) {
291
292
  return error instanceof ConflictError;
292
293
  }
294
+ const UnauthenticatedErrorCode = {
295
+ AuthenticationRequired: "AUTHENTICATION_REQUIRED",
296
+ InvalidCredentials: "INVALID_CREDENTIALS"
297
+ };
293
298
  var UnauthenticatedError = class extends ApplicationError {
294
299
  name = "UnauthenticatedError";
295
300
  constructor(code, message, cause) {
@@ -300,6 +305,7 @@ var UnauthenticatedError = class extends ApplicationError {
300
305
  function isUnauthenticatedError(error) {
301
306
  return error instanceof UnauthenticatedError;
302
307
  }
308
+ const ForbiddenErrorCode = { InsufficientPermissions: "INSUFFICIENT_PERMISSIONS" };
303
309
  var ForbiddenError = class extends ApplicationError {
304
310
  name = "ForbiddenError";
305
311
  constructor(code, message, cause) {
@@ -549,6 +555,60 @@ async function applyAction({ container }) {
549
555
  });
550
556
  }
551
557
 
558
+ //#endregion
559
+ //#region src/core/adapters/kintone/wrapKintoneError.ts
560
+ const KINTONE_REVISION_CONFLICT_CODE = "GAIA_CO02";
561
+ const KINTONE_MAINTENANCE_MODE_CODE = "GAIA_NO02";
562
+ function formatMessage(message, error) {
563
+ if (error instanceof KintoneRestAPIError && error.message) return `${message}: ${error.message}`;
564
+ if (error instanceof Error && error.message) return `${message}: ${error.message}`;
565
+ return message;
566
+ }
567
+ /**
568
+ * Converts an unknown error thrown during a kintone API call into the
569
+ * appropriate application-layer error.
570
+ *
571
+ * - {@link BusinessRuleError} and {@link ApplicationError} (including all
572
+ * subclasses such as {@link SystemError}, {@link ValidationError}, etc.)
573
+ * are re-thrown as-is so that errors raised inside a try block are never
574
+ * silently converted.
575
+ * - {@link KintoneRestAPIError} is mapped by HTTP status (401/403/404/409/400)
576
+ * and kintone-specific codes (`GAIA_CO02` revision conflict, `GAIA_NO02`
577
+ * maintenance mode).
578
+ * - Everything else becomes a {@link SystemError} with code `ExternalApiError`.
579
+ *
580
+ * **Note on 400 mapping**: All 400 errors (except `GAIA_CO02`) are mapped to
581
+ * {@link ValidationError}. This is intentionally simplified — some 400 codes
582
+ * (e.g. `GAIA_RE18` record limit, `GAIA_AP01` app unavailable) may have
583
+ * different semantics, but for this CLI tool a single `ValidationError`
584
+ * with the original kintone message in the error detail is sufficient.
585
+ *
586
+ * @throws {UnauthenticatedError} when kintone returns 401
587
+ * @throws {ForbiddenError} when kintone returns 403 (non-GAIA_NO02)
588
+ * @throws {SystemError} when kintone returns 403 with GAIA_NO02 (maintenance mode)
589
+ * @throws {NotFoundError} when kintone returns 404
590
+ * @throws {ConflictError} when kintone returns 409 or GAIA_CO02
591
+ * @throws {ValidationError} when kintone returns 400 (non-GAIA_CO02)
592
+ * @throws {SystemError} for all other kintone errors and unknown errors
593
+ */
594
+ function wrapKintoneError(error, message) {
595
+ if (isBusinessRuleError(error)) throw error;
596
+ if (error instanceof ApplicationError) throw error;
597
+ const detail = formatMessage(message, error);
598
+ if (error instanceof KintoneRestAPIError) {
599
+ if (error.status === 401) throw new UnauthenticatedError(UnauthenticatedErrorCode.InvalidCredentials, detail, error);
600
+ if (error.status === 403) {
601
+ if (error.code === KINTONE_MAINTENANCE_MODE_CODE) throw new SystemError(SystemErrorCode.ExternalApiError, `${detail} (app is in maintenance mode — GAIA_NO02)`, error);
602
+ throw new ForbiddenError(ForbiddenErrorCode.InsufficientPermissions, detail, error);
603
+ }
604
+ if (error.status === 404) throw new NotFoundError(NotFoundErrorCode.NotFound, detail, error);
605
+ if (error.code === KINTONE_REVISION_CONFLICT_CODE) throw new ConflictError(ConflictErrorCode.Conflict, `${detail} (revision conflict — GAIA_CO02). Please retry the operation.`, error);
606
+ if (error.status === 409) throw new ConflictError(ConflictErrorCode.Conflict, detail, error);
607
+ if (error.status === 400) throw new ValidationError(ValidationErrorCode.InvalidInput, detail, error);
608
+ }
609
+ throw new SystemError(SystemErrorCode.ExternalApiError, detail, error);
610
+ }
611
+
552
612
  //#endregion
553
613
  //#region src/core/adapters/kintone/actionConfigurator.ts
554
614
  function fromKintoneDestApp(raw) {
@@ -634,9 +694,7 @@ var KintoneActionConfigurator = class {
634
694
  revision: response.revision
635
695
  };
636
696
  } catch (error) {
637
- if (isBusinessRuleError(error)) throw error;
638
- if (error instanceof SystemError) throw error;
639
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get app actions", error);
697
+ throw wrapKintoneError(error, "Failed to get app actions");
640
698
  }
641
699
  }
642
700
  async updateActions(params) {
@@ -650,15 +708,22 @@ var KintoneActionConfigurator = class {
650
708
  if (params.revision !== void 0) requestParams.revision = params.revision;
651
709
  return { revision: (await this.client.app.updateAppActions(requestParams)).revision };
652
710
  } catch (error) {
653
- if (isBusinessRuleError(error)) throw error;
654
- if (error instanceof SystemError) throw error;
655
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update app actions", error);
711
+ throw wrapKintoneError(error, "Failed to update app actions");
656
712
  }
657
713
  }
658
714
  };
659
715
 
660
716
  //#endregion
661
717
  //#region src/core/adapters/kintone/appDeployer.ts
718
+ const VALID_DEPLOY_STATUSES = new Set([
719
+ "PROCESSING",
720
+ "SUCCESS",
721
+ "FAIL",
722
+ "CANCEL"
723
+ ]);
724
+ function isDeployStatus(value) {
725
+ return VALID_DEPLOY_STATUSES.has(value);
726
+ }
662
727
  const DEFAULT_POLL_INTERVAL_MS = 1e3;
663
728
  const DEFAULT_MAX_RETRIES = 180;
664
729
  var KintoneAppDeployer = class {
@@ -673,12 +738,10 @@ var KintoneAppDeployer = class {
673
738
  async deploy() {
674
739
  try {
675
740
  await this.client.app.deployApp({ apps: [{ app: this.appId }] });
676
- await this.waitForDeployment();
677
741
  } catch (error) {
678
- if (isBusinessRuleError(error)) throw error;
679
- if (error instanceof SystemError) throw error;
680
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to deploy app", error);
742
+ throw wrapKintoneError(error, "Failed to deploy app");
681
743
  }
744
+ await this.waitForDeployment();
682
745
  }
683
746
  async waitForDeployment() {
684
747
  let lastPollError;
@@ -692,20 +755,21 @@ var KintoneAppDeployer = class {
692
755
  consecutivePollFailures = 0;
693
756
  } catch (error) {
694
757
  if (isBusinessRuleError(error)) throw error;
695
- if (error instanceof SystemError) throw error;
758
+ if (error instanceof ApplicationError) throw error;
696
759
  lastPollError = error;
697
760
  consecutivePollFailures++;
698
761
  if (consecutivePollFailures >= maxConsecutivePollFailures) throw new SystemError(SystemErrorCode.ExternalApiError, `Deploy status polling failed ${maxConsecutivePollFailures} consecutive times`, lastPollError);
699
762
  continue;
700
763
  }
701
- const status = apps[0]?.status;
702
- switch (status) {
764
+ if (apps.length === 0) throw new SystemError(SystemErrorCode.ExternalApiError, "Deploy status response contained no apps");
765
+ const rawStatus = apps[0].status;
766
+ if (rawStatus === void 0) throw new SystemError(SystemErrorCode.ExternalApiError, "Deploy status unavailable");
767
+ if (!isDeployStatus(rawStatus)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected deploy status: ${rawStatus}`);
768
+ switch (rawStatus) {
703
769
  case "SUCCESS": return;
704
770
  case "FAIL": throw new SystemError(SystemErrorCode.ExternalApiError, "App deployment failed");
705
771
  case "CANCEL": throw new SystemError(SystemErrorCode.ExternalApiError, "App deployment was cancelled");
706
772
  case "PROCESSING": continue;
707
- case void 0: throw new SystemError(SystemErrorCode.ExternalApiError, "Deploy status unavailable");
708
- default: throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected deploy status: ${status}`);
709
773
  }
710
774
  }
711
775
  throw new SystemError(SystemErrorCode.ExternalApiError, "App deployment timed out", lastPollError);
@@ -834,9 +898,7 @@ var KintoneCustomizationConfigurator = class {
834
898
  revision: response.revision
835
899
  };
836
900
  } catch (error) {
837
- if (isBusinessRuleError(error)) throw error;
838
- if (error instanceof SystemError) throw error;
839
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get app customization", error);
901
+ throw wrapKintoneError(error, "Failed to get app customization");
840
902
  }
841
903
  }
842
904
  async updateCustomization(params) {
@@ -857,9 +919,7 @@ var KintoneCustomizationConfigurator = class {
857
919
  const response = await this.client.app.updateAppCustomize(requestParams);
858
920
  return { revision: String(response.revision) };
859
921
  } catch (error) {
860
- if (isBusinessRuleError(error)) throw error;
861
- if (error instanceof SystemError) throw error;
862
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update app customization", error);
922
+ throw wrapKintoneError(error, "Failed to update app customization");
863
923
  }
864
924
  }
865
925
  };
@@ -871,31 +931,77 @@ var KintoneFileDownloader = class {
871
931
  this.client = client;
872
932
  }
873
933
  async download(fileKey) {
874
- if (!fileKey) throw new SystemError(SystemErrorCode.ExternalApiError, "fileKey must not be empty");
934
+ if (!fileKey) throw new ValidationError(ValidationErrorCode.InvalidInput, "fileKey must not be empty");
875
935
  try {
876
936
  return await this.client.file.downloadFile({ fileKey });
877
937
  } catch (error) {
878
- if (isBusinessRuleError(error)) throw error;
879
- if (error instanceof SystemError) throw error;
880
- throw new SystemError(SystemErrorCode.ExternalApiError, `Failed to download file: ${fileKey}`, error);
938
+ throw wrapKintoneError(error, `Failed to download file: ${fileKey}`);
881
939
  }
882
940
  }
883
941
  };
884
942
 
943
+ //#endregion
944
+ //#region src/lib/safePath.ts
945
+ /**
946
+ * Returns `true` if {@link targetPath} resolves to a location within
947
+ * {@link baseDir}. Follows symlinks via `realpathSync` for both the base
948
+ * directory and the target path when they exist on disk, falling back to
949
+ * textual `resolve` otherwise (e.g. in tests with virtual paths or when
950
+ * the target does not yet exist).
951
+ *
952
+ * {@link targetPath} may be either a relative path (resolved against
953
+ * {@link baseDir}) or an absolute path (used as-is by `path.resolve`).
954
+ * When callers have already resolved the path via `path.resolve(baseDir, x)`,
955
+ * passing the result as an absolute path is safe — `resolve(base, absPath)`
956
+ * returns `absPath` unchanged.
957
+ *
958
+ * **Limitation — symlinks**: If the target path does not exist on disk,
959
+ * symlink components within it cannot be resolved. The check falls back
960
+ * to a textual prefix comparison, which still catches `..` traversal but
961
+ * cannot detect symlink-based escapes for non-existent paths.
962
+ *
963
+ * **Limitation — TOCTOU**: This check is inherently subject to a
964
+ * time-of-check-to-time-of-use race: the filesystem may change between
965
+ * this call and the subsequent file operation. This is acceptable for a
966
+ * CLI tool where the user controls the local environment.
967
+ *
968
+ * **Note**: This function performs synchronous I/O (`realpathSync`).
969
+ *
970
+ * Callers decide how to handle the result.
971
+ */
972
+ function isSafePath(targetPath, baseDir) {
973
+ if (targetPath.includes("\0") || baseDir.includes("\0")) return false;
974
+ let resolvedBase;
975
+ try {
976
+ resolvedBase = realpathSync(baseDir);
977
+ } catch {
978
+ resolvedBase = resolve(baseDir);
979
+ }
980
+ const textualTarget = resolve(resolvedBase, targetPath);
981
+ let resolvedTarget;
982
+ try {
983
+ resolvedTarget = realpathSync(textualTarget);
984
+ } catch {
985
+ resolvedTarget = textualTarget;
986
+ }
987
+ return resolvedTarget.startsWith(`${resolvedBase}/`) || resolvedTarget === resolvedBase;
988
+ }
989
+
885
990
  //#endregion
886
991
  //#region src/core/adapters/kintone/fileUploader.ts
887
992
  var KintoneFileUploader = class {
888
- constructor(client) {
993
+ constructor(client, baseDir) {
889
994
  this.client = client;
995
+ this.baseDir = baseDir;
890
996
  }
891
997
  async upload(filePath) {
892
- if (!filePath) throw new SystemError(SystemErrorCode.ExternalApiError, "filePath must not be empty");
998
+ if (!filePath) throw new ValidationError(ValidationErrorCode.InvalidInput, "filePath must not be empty");
999
+ const resolvedPath = resolve(this.baseDir, filePath);
1000
+ if (!isSafePath(resolvedPath, this.baseDir)) throw new ValidationError(ValidationErrorCode.InvalidInput, `Path traversal detected: "${resolvedPath}" escapes base directory "${this.baseDir}"`);
893
1001
  try {
894
- return { fileKey: (await this.client.file.uploadFile({ file: { path: filePath } })).fileKey };
1002
+ return { fileKey: (await this.client.file.uploadFile({ file: { path: resolvedPath } })).fileKey };
895
1003
  } catch (error) {
896
- if (isBusinessRuleError(error)) throw error;
897
- if (error instanceof SystemError) throw error;
898
- throw new SystemError(SystemErrorCode.ExternalApiError, `Failed to upload file: ${filePath}`, error);
1004
+ throw wrapKintoneError(error, `Failed to upload file: ${filePath}`);
899
1005
  }
900
1006
  }
901
1007
  };
@@ -976,10 +1082,6 @@ const DECORATION_TYPES$1 = new Set([
976
1082
  "SPACER",
977
1083
  "HR"
978
1084
  ]);
979
- const KINTONE_REVISION_CONFLICT_CODE = "GAIA_CO02";
980
- function isRevisionConflict(error) {
981
- return error instanceof KintoneRestAPIError && error.code === KINTONE_REVISION_CONFLICT_CODE;
982
- }
983
1085
  /**
984
1086
  * Tracks the latest known kintone form revision for optimistic concurrency control.
985
1087
  *
@@ -992,10 +1094,13 @@ function isRevisionConflict(error) {
992
1094
  var RevisionTracker = class {
993
1095
  revision = void 0;
994
1096
  track(revision) {
995
- if (this.revision === void 0 || Number(revision) > Number(this.revision)) this.revision = revision;
1097
+ if (revision === "") throw new SystemError(SystemErrorCode.ExternalApiError, "Unexpected empty revision from kintone API");
1098
+ const revisionNum = Number(revision);
1099
+ if (!Number.isFinite(revisionNum)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected non-numeric revision from kintone API: "${revision}"`);
1100
+ if (this.revision === void 0 || revisionNum > this.revision) this.revision = revisionNum;
996
1101
  }
997
1102
  get current() {
998
- return this.revision;
1103
+ return this.revision !== void 0 ? String(this.revision) : void 0;
999
1104
  }
1000
1105
  };
1001
1106
  function assertRecord(value, fieldPath) {
@@ -1267,7 +1372,7 @@ var KintoneFormConfigurator = class {
1267
1372
  app: this.appId,
1268
1373
  preview: true
1269
1374
  });
1270
- this.revisionTracker.track(revision);
1375
+ if (revision) this.revisionTracker.track(revision);
1271
1376
  const fields = /* @__PURE__ */ new Map();
1272
1377
  for (const [code, prop] of Object.entries(properties)) {
1273
1378
  const kintoneProp = prop;
@@ -1278,9 +1383,7 @@ var KintoneFormConfigurator = class {
1278
1383
  }
1279
1384
  return fields;
1280
1385
  } catch (error) {
1281
- if (isBusinessRuleError(error)) throw error;
1282
- if (error instanceof SystemError) throw error;
1283
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get form fields", error);
1386
+ throw wrapKintoneError(error, "Failed to get form fields");
1284
1387
  }
1285
1388
  }
1286
1389
  async addFields(fields) {
@@ -1294,10 +1397,7 @@ var KintoneFormConfigurator = class {
1294
1397
  });
1295
1398
  if (response.revision) this.revisionTracker.track(response.revision);
1296
1399
  } catch (error) {
1297
- if (isBusinessRuleError(error)) throw error;
1298
- if (error instanceof SystemError) throw error;
1299
- if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
1300
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to add form fields", error);
1400
+ throw wrapKintoneError(error, "Failed to add form fields");
1301
1401
  }
1302
1402
  }
1303
1403
  async updateFields(fields) {
@@ -1311,10 +1411,7 @@ var KintoneFormConfigurator = class {
1311
1411
  });
1312
1412
  if (response.revision) this.revisionTracker.track(response.revision);
1313
1413
  } catch (error) {
1314
- if (isBusinessRuleError(error)) throw error;
1315
- if (error instanceof SystemError) throw error;
1316
- if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
1317
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update form fields", error);
1414
+ throw wrapKintoneError(error, "Failed to update form fields");
1318
1415
  }
1319
1416
  }
1320
1417
  async deleteFields(fieldCodes) {
@@ -1326,10 +1423,7 @@ var KintoneFormConfigurator = class {
1326
1423
  });
1327
1424
  if (response.revision) this.revisionTracker.track(response.revision);
1328
1425
  } catch (error) {
1329
- if (isBusinessRuleError(error)) throw error;
1330
- if (error instanceof SystemError) throw error;
1331
- if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
1332
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to delete form fields", error);
1426
+ throw wrapKintoneError(error, "Failed to delete form fields");
1333
1427
  }
1334
1428
  }
1335
1429
  async getLayout() {
@@ -1338,12 +1432,10 @@ var KintoneFormConfigurator = class {
1338
1432
  app: this.appId,
1339
1433
  preview: true
1340
1434
  });
1341
- this.revisionTracker.track(response.revision);
1435
+ if (response.revision) this.revisionTracker.track(response.revision);
1342
1436
  return response.layout.map(fromKintoneLayoutItem);
1343
1437
  } catch (error) {
1344
- if (isBusinessRuleError(error)) throw error;
1345
- if (error instanceof SystemError) throw error;
1346
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get form layout", error);
1438
+ throw wrapKintoneError(error, "Failed to get form layout");
1347
1439
  }
1348
1440
  }
1349
1441
  async updateLayout(layout) {
@@ -1356,10 +1448,7 @@ var KintoneFormConfigurator = class {
1356
1448
  });
1357
1449
  if (response.revision) this.revisionTracker.track(response.revision);
1358
1450
  } catch (error) {
1359
- if (isBusinessRuleError(error)) throw error;
1360
- if (error instanceof SystemError) throw error;
1361
- if (isRevisionConflict(error)) throw new ConflictError(ConflictErrorCode.Conflict, "Form configuration was modified by another process. Please retry the operation.", error);
1362
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update form layout", error);
1451
+ throw wrapKintoneError(error, "Failed to update form layout");
1363
1452
  }
1364
1453
  }
1365
1454
  };
@@ -1480,9 +1569,7 @@ var KintoneRecordManager = class {
1480
1569
  };
1481
1570
  });
1482
1571
  } catch (error) {
1483
- if (isBusinessRuleError(error)) throw error;
1484
- if (error instanceof SystemError) throw error;
1485
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get records", error);
1572
+ throw wrapKintoneError(error, "Failed to get records");
1486
1573
  }
1487
1574
  }
1488
1575
  async addRecords(records) {
@@ -1493,9 +1580,7 @@ var KintoneRecordManager = class {
1493
1580
  records: records.map(toKintoneRecord)
1494
1581
  });
1495
1582
  } catch (error) {
1496
- if (isBusinessRuleError(error)) throw error;
1497
- if (error instanceof SystemError) throw error;
1498
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to add records", error);
1583
+ throw wrapKintoneError(error, "Failed to add records");
1499
1584
  }
1500
1585
  }
1501
1586
  async updateRecords(records) {
@@ -1509,9 +1594,7 @@ var KintoneRecordManager = class {
1509
1594
  }))
1510
1595
  });
1511
1596
  } catch (error) {
1512
- if (isBusinessRuleError(error)) throw error;
1513
- if (error instanceof SystemError) throw error;
1514
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update records", error);
1597
+ throw wrapKintoneError(error, "Failed to update records");
1515
1598
  }
1516
1599
  }
1517
1600
  async deleteAllRecords() {
@@ -1528,9 +1611,7 @@ var KintoneRecordManager = class {
1528
1611
  });
1529
1612
  return { deletedCount: records.length };
1530
1613
  } catch (error) {
1531
- if (isBusinessRuleError(error)) throw error;
1532
- if (error instanceof SystemError) throw error;
1533
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to delete all records", error);
1614
+ throw wrapKintoneError(error, "Failed to delete all records");
1534
1615
  }
1535
1616
  }
1536
1617
  };
@@ -1544,10 +1625,15 @@ function createLocalFileCustomizationStorage(filePath) {
1544
1625
  //#endregion
1545
1626
  //#region src/core/adapters/local/fileWriter.ts
1546
1627
  var LocalFileWriter = class {
1628
+ constructor(baseDir = process.cwd()) {
1629
+ this.baseDir = baseDir;
1630
+ }
1547
1631
  async write(filePath, data) {
1632
+ const resolvedPath = resolve(this.baseDir, filePath);
1633
+ if (!isSafePath(resolvedPath, this.baseDir)) throw new ValidationError(ValidationErrorCode.InvalidInput, `Path traversal detected: "${resolvedPath}" escapes base directory "${this.baseDir}"`);
1548
1634
  try {
1549
- await mkdir(dirname(filePath), { recursive: true });
1550
- await writeFile(filePath, Buffer.from(new Uint8Array(data)));
1635
+ await mkdir(dirname(resolvedPath), { recursive: true });
1636
+ await writeFile(resolvedPath, Buffer.from(new Uint8Array(data)));
1551
1637
  } catch (error) {
1552
1638
  throw new SystemError(SystemErrorCode.StorageError, `Failed to write file: ${filePath}`, error);
1553
1639
  }
@@ -1594,9 +1680,9 @@ function createCustomizationCliContainer(config) {
1594
1680
  return {
1595
1681
  customizationConfigurator: new KintoneCustomizationConfigurator(client, config.appId),
1596
1682
  customizationStorage: createLocalFileCustomizationStorage(config.customizeFilePath),
1597
- fileUploader: new KintoneFileUploader(client),
1683
+ fileUploader: new KintoneFileUploader(client, process.cwd()),
1598
1684
  fileDownloader: new KintoneFileDownloader(client),
1599
- fileWriter: new LocalFileWriter(),
1685
+ fileWriter: new LocalFileWriter(process.cwd()),
1600
1686
  appDeployer: new KintoneAppDeployer(client, config.appId)
1601
1687
  };
1602
1688
  }
@@ -2936,9 +3022,7 @@ var KintoneAdminNotesConfigurator = class {
2936
3022
  revision: response.revision
2937
3023
  };
2938
3024
  } catch (error) {
2939
- if (isBusinessRuleError(error)) throw error;
2940
- if (error instanceof SystemError) throw error;
2941
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get admin notes", error);
3025
+ throw wrapKintoneError(error, "Failed to get admin notes");
2942
3026
  }
2943
3027
  }
2944
3028
  async updateAdminNotes(params) {
@@ -2951,9 +3035,7 @@ var KintoneAdminNotesConfigurator = class {
2951
3035
  if (params.revision !== void 0) requestParams.revision = params.revision;
2952
3036
  return { revision: (await this.client.app.updateAdminNotes(requestParams)).revision };
2953
3037
  } catch (error) {
2954
- if (isBusinessRuleError(error)) throw error;
2955
- if (error instanceof SystemError) throw error;
2956
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update admin notes", error);
3038
+ throw wrapKintoneError(error, "Failed to update admin notes");
2957
3039
  }
2958
3040
  }
2959
3041
  };
@@ -3303,9 +3385,7 @@ var KintoneAppPermissionConfigurator = class {
3303
3385
  revision: response.revision
3304
3386
  };
3305
3387
  } catch (error) {
3306
- if (isBusinessRuleError(error)) throw error;
3307
- if (error instanceof SystemError) throw error;
3308
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get app ACL", error);
3388
+ throw wrapKintoneError(error, "Failed to get app ACL");
3309
3389
  }
3310
3390
  }
3311
3391
  async updateAppPermissions(params) {
@@ -3317,9 +3397,7 @@ var KintoneAppPermissionConfigurator = class {
3317
3397
  if (params.revision !== void 0) requestParams.revision = params.revision;
3318
3398
  return { revision: (await this.client.app.updateAppAcl(requestParams)).revision };
3319
3399
  } catch (error) {
3320
- if (isBusinessRuleError(error)) throw error;
3321
- if (error instanceof SystemError) throw error;
3322
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update app ACL", error);
3400
+ throw wrapKintoneError(error, "Failed to update app ACL");
3323
3401
  }
3324
3402
  }
3325
3403
  };
@@ -4212,9 +4290,7 @@ var KintoneFieldPermissionConfigurator = class {
4212
4290
  revision: response.revision
4213
4291
  };
4214
4292
  } catch (error) {
4215
- if (isBusinessRuleError(error)) throw error;
4216
- if (error instanceof SystemError) throw error;
4217
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get field ACL", error);
4293
+ throw wrapKintoneError(error, "Failed to get field ACL");
4218
4294
  }
4219
4295
  }
4220
4296
  async updateFieldPermissions(params) {
@@ -4226,9 +4302,7 @@ var KintoneFieldPermissionConfigurator = class {
4226
4302
  if (params.revision !== void 0) requestParams.revision = params.revision;
4227
4303
  return { revision: (await this.client.app.updateFieldAcl(requestParams)).revision };
4228
4304
  } catch (error) {
4229
- if (isBusinessRuleError(error)) throw error;
4230
- if (error instanceof SystemError) throw error;
4231
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update field ACL", error);
4305
+ throw wrapKintoneError(error, "Failed to update field ACL");
4232
4306
  }
4233
4307
  }
4234
4308
  };
@@ -4557,6 +4631,14 @@ var field_acl_default = define({
4557
4631
  run: () => {}
4558
4632
  });
4559
4633
 
4634
+ //#endregion
4635
+ //#region src/core/adapters/kintone/parseKintoneIntegerField.ts
4636
+ function parseKintoneIntegerField(raw, fieldName) {
4637
+ const n = Number(raw);
4638
+ if (!Number.isFinite(n) || !Number.isInteger(n)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected non-integer ${fieldName} from kintone API: ${raw}`);
4639
+ return n;
4640
+ }
4641
+
4560
4642
  //#endregion
4561
4643
  //#region src/core/adapters/kintone/generalSettingsConfigurator.ts
4562
4644
  const VALID_THEMES$1 = new Set([
@@ -4601,8 +4683,8 @@ function fromKintoneTitleField(raw) {
4601
4683
  function fromKintoneNumberPrecision(raw) {
4602
4684
  if (!VALID_ROUNDING_MODES$1.has(raw.roundingMode)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected roundingMode from kintone API: ${raw.roundingMode}`);
4603
4685
  return {
4604
- digits: Number(raw.digits),
4605
- decimalPlaces: Number(raw.decimalPlaces),
4686
+ digits: parseKintoneIntegerField(raw.digits, "digits"),
4687
+ decimalPlaces: parseKintoneIntegerField(raw.decimalPlaces, "decimalPlaces"),
4606
4688
  roundingMode: raw.roundingMode
4607
4689
  };
4608
4690
  }
@@ -4620,7 +4702,7 @@ function fromKintoneSettings(raw) {
4620
4702
  ...raw.enableDuplicateRecord !== void 0 ? { enableDuplicateRecord: raw.enableDuplicateRecord } : {},
4621
4703
  ...raw.enableInlineRecordEditing !== void 0 ? { enableInlineRecordEditing: raw.enableInlineRecordEditing } : {},
4622
4704
  ...raw.numberPrecision !== void 0 ? { numberPrecision: fromKintoneNumberPrecision(raw.numberPrecision) } : {},
4623
- ...raw.firstMonthOfFiscalYear !== void 0 ? { firstMonthOfFiscalYear: Number(raw.firstMonthOfFiscalYear) } : {}
4705
+ ...raw.firstMonthOfFiscalYear !== void 0 ? { firstMonthOfFiscalYear: parseKintoneIntegerField(raw.firstMonthOfFiscalYear, "firstMonthOfFiscalYear") } : {}
4624
4706
  };
4625
4707
  }
4626
4708
  function toKintoneIcon(icon) {
@@ -4673,9 +4755,7 @@ var KintoneGeneralSettingsConfigurator = class {
4673
4755
  revision: raw.revision ?? "-1"
4674
4756
  };
4675
4757
  } catch (error) {
4676
- if (isBusinessRuleError(error)) throw error;
4677
- if (error instanceof SystemError) throw error;
4678
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get general settings", error);
4758
+ throw wrapKintoneError(error, "Failed to get general settings");
4679
4759
  }
4680
4760
  }
4681
4761
  async updateGeneralSettings(params) {
@@ -4687,9 +4767,7 @@ var KintoneGeneralSettingsConfigurator = class {
4687
4767
  if (params.revision !== void 0) requestParams.revision = params.revision;
4688
4768
  return { revision: (await this.client.app.updateAppSettings(requestParams)).revision };
4689
4769
  } catch (error) {
4690
- if (isBusinessRuleError(error)) throw error;
4691
- if (error instanceof SystemError) throw error;
4692
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update general settings", error);
4770
+ throw wrapKintoneError(error, "Failed to update general settings");
4693
4771
  }
4694
4772
  }
4695
4773
  };
@@ -4764,8 +4842,7 @@ function fromKintonePerRecordNotification(raw) {
4764
4842
  };
4765
4843
  }
4766
4844
  function fromKintoneReminderNotification(raw) {
4767
- const daysLater = Number(raw.timing.daysLater);
4768
- if (!Number.isFinite(daysLater)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected non-numeric daysLater from kintone API: ${raw.timing.daysLater}`);
4845
+ const daysLater = parseKintoneIntegerField(raw.timing.daysLater, "daysLater");
4769
4846
  const result = {
4770
4847
  code: raw.timing.code,
4771
4848
  daysLater,
@@ -4774,8 +4851,7 @@ function fromKintoneReminderNotification(raw) {
4774
4851
  targets: raw.targets.map(fromKintoneTarget)
4775
4852
  };
4776
4853
  if ("hoursLater" in raw.timing) {
4777
- const hoursLater = Number(raw.timing.hoursLater);
4778
- if (!Number.isFinite(hoursLater)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected non-numeric hoursLater from kintone API: ${raw.timing.hoursLater}`);
4854
+ const hoursLater = parseKintoneIntegerField(raw.timing.hoursLater, "hoursLater");
4779
4855
  return {
4780
4856
  ...result,
4781
4857
  hoursLater
@@ -4850,9 +4926,7 @@ var KintoneNotificationConfigurator = class {
4850
4926
  revision: response.revision
4851
4927
  };
4852
4928
  } catch (error) {
4853
- if (isBusinessRuleError(error)) throw error;
4854
- if (error instanceof SystemError) throw error;
4855
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get general notifications", error);
4929
+ throw wrapKintoneError(error, "Failed to get general notifications");
4856
4930
  }
4857
4931
  }
4858
4932
  async updateGeneralNotifications(params) {
@@ -4865,9 +4939,7 @@ var KintoneNotificationConfigurator = class {
4865
4939
  if (params.revision !== void 0) requestParams.revision = params.revision;
4866
4940
  return { revision: (await this.client.app.updateGeneralNotifications(requestParams)).revision };
4867
4941
  } catch (error) {
4868
- if (isBusinessRuleError(error)) throw error;
4869
- if (error instanceof SystemError) throw error;
4870
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update general notifications", error);
4942
+ throw wrapKintoneError(error, "Failed to update general notifications");
4871
4943
  }
4872
4944
  }
4873
4945
  async getPerRecordNotifications() {
@@ -4881,9 +4953,7 @@ var KintoneNotificationConfigurator = class {
4881
4953
  revision: response.revision
4882
4954
  };
4883
4955
  } catch (error) {
4884
- if (isBusinessRuleError(error)) throw error;
4885
- if (error instanceof SystemError) throw error;
4886
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get per-record notifications", error);
4956
+ throw wrapKintoneError(error, "Failed to get per-record notifications");
4887
4957
  }
4888
4958
  }
4889
4959
  async updatePerRecordNotifications(params) {
@@ -4895,9 +4965,7 @@ var KintoneNotificationConfigurator = class {
4895
4965
  if (params.revision !== void 0) requestParams.revision = params.revision;
4896
4966
  return { revision: (await this.client.app.updatePerRecordNotifications(requestParams)).revision };
4897
4967
  } catch (error) {
4898
- if (isBusinessRuleError(error)) throw error;
4899
- if (error instanceof SystemError) throw error;
4900
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update per-record notifications", error);
4968
+ throw wrapKintoneError(error, "Failed to update per-record notifications");
4901
4969
  }
4902
4970
  }
4903
4971
  async getReminderNotifications() {
@@ -4913,9 +4981,7 @@ var KintoneNotificationConfigurator = class {
4913
4981
  revision: response.revision
4914
4982
  };
4915
4983
  } catch (error) {
4916
- if (isBusinessRuleError(error)) throw error;
4917
- if (error instanceof SystemError) throw error;
4918
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get reminder notifications", error);
4984
+ throw wrapKintoneError(error, "Failed to get reminder notifications");
4919
4985
  }
4920
4986
  }
4921
4987
  async updateReminderNotifications(params) {
@@ -4928,9 +4994,7 @@ var KintoneNotificationConfigurator = class {
4928
4994
  if (params.revision !== void 0) requestParams.revision = params.revision;
4929
4995
  return { revision: (await this.client.app.updateReminderNotifications(requestParams)).revision };
4930
4996
  } catch (error) {
4931
- if (isBusinessRuleError(error)) throw error;
4932
- if (error instanceof SystemError) throw error;
4933
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update reminder notifications", error);
4997
+ throw wrapKintoneError(error, "Failed to update reminder notifications");
4934
4998
  }
4935
4999
  }
4936
5000
  };
@@ -4974,22 +5038,18 @@ var KintonePluginConfigurator = class {
4974
5038
  revision: response.revision
4975
5039
  };
4976
5040
  } catch (error) {
4977
- if (isBusinessRuleError(error)) throw error;
4978
- if (error instanceof SystemError) throw error;
4979
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get plugins", error);
5041
+ throw wrapKintoneError(error, "Failed to get plugins");
4980
5042
  }
4981
5043
  }
4982
5044
  async addPlugins(params) {
4983
5045
  try {
4984
5046
  return { revision: (await this.client.app.addPlugins({
4985
5047
  app: this.appId,
4986
- ids: params.ids,
5048
+ ids: [...params.ids],
4987
5049
  revision: params.revision
4988
5050
  })).revision };
4989
5051
  } catch (error) {
4990
- if (isBusinessRuleError(error)) throw error;
4991
- if (error instanceof SystemError) throw error;
4992
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to add plugins", error);
5052
+ throw wrapKintoneError(error, "Failed to add plugins");
4993
5053
  }
4994
5054
  }
4995
5055
  };
@@ -5122,9 +5182,7 @@ var KintoneProcessManagementConfigurator = class {
5122
5182
  revision: response.revision
5123
5183
  };
5124
5184
  } catch (error) {
5125
- if (isBusinessRuleError(error)) throw error;
5126
- if (error instanceof SystemError) throw error;
5127
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get process management settings", error);
5185
+ throw wrapKintoneError(error, "Failed to get process management settings");
5128
5186
  }
5129
5187
  }
5130
5188
  async updateProcessManagement(params) {
@@ -5140,9 +5198,7 @@ var KintoneProcessManagementConfigurator = class {
5140
5198
  if (params.revision !== void 0) requestParams.revision = params.revision;
5141
5199
  return { revision: (await this.client.app.updateProcessManagement(requestParams)).revision };
5142
5200
  } catch (error) {
5143
- if (isBusinessRuleError(error)) throw error;
5144
- if (error instanceof SystemError) throw error;
5145
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update process management settings", error);
5201
+ throw wrapKintoneError(error, "Failed to update process management settings");
5146
5202
  }
5147
5203
  }
5148
5204
  };
@@ -5225,9 +5281,7 @@ var KintoneRecordPermissionConfigurator = class {
5225
5281
  revision: response.revision
5226
5282
  };
5227
5283
  } catch (error) {
5228
- if (isBusinessRuleError(error)) throw error;
5229
- if (error instanceof SystemError) throw error;
5230
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get record ACL", error);
5284
+ throw wrapKintoneError(error, "Failed to get record ACL");
5231
5285
  }
5232
5286
  }
5233
5287
  async updateRecordPermissions(params) {
@@ -5239,9 +5293,7 @@ var KintoneRecordPermissionConfigurator = class {
5239
5293
  if (params.revision !== void 0) requestParams.revision = params.revision;
5240
5294
  return { revision: (await this.client.app.updateRecordAcl(requestParams)).revision };
5241
5295
  } catch (error) {
5242
- if (isBusinessRuleError(error)) throw error;
5243
- if (error instanceof SystemError) throw error;
5244
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update record ACL", error);
5296
+ throw wrapKintoneError(error, "Failed to update record ACL");
5245
5297
  }
5246
5298
  }
5247
5299
  };
@@ -5514,9 +5566,7 @@ var KintoneReportConfigurator = class {
5514
5566
  revision: response.revision
5515
5567
  };
5516
5568
  } catch (error) {
5517
- if (isBusinessRuleError(error)) throw error;
5518
- if (error instanceof SystemError) throw error;
5519
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get reports", error);
5569
+ throw wrapKintoneError(error, "Failed to get reports");
5520
5570
  }
5521
5571
  }
5522
5572
  async updateReports(params) {
@@ -5530,9 +5580,7 @@ var KintoneReportConfigurator = class {
5530
5580
  if (params.revision !== void 0) requestParams.revision = params.revision;
5531
5581
  return { revision: (await this.client.app.updateReports(requestParams)).revision };
5532
5582
  } catch (error) {
5533
- if (isBusinessRuleError(error)) throw error;
5534
- if (error instanceof SystemError) throw error;
5535
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update reports", error);
5583
+ throw wrapKintoneError(error, "Failed to update reports");
5536
5584
  }
5537
5585
  }
5538
5586
  };
@@ -5582,21 +5630,17 @@ function fromKintoneView(name, raw) {
5582
5630
  }
5583
5631
  return {
5584
5632
  type: raw.type,
5585
- index: (() => {
5586
- const idx = typeof raw.index === "string" ? Number(raw.index) : raw.index;
5587
- if (!Number.isFinite(idx)) throw new SystemError(SystemErrorCode.ExternalApiError, `Unexpected non-numeric index from kintone API: ${raw.index}`);
5588
- return idx;
5589
- })(),
5633
+ index: parseKintoneIntegerField(raw.index, "index"),
5590
5634
  name,
5591
- ...raw.builtinType !== void 0 && { builtinType: raw.builtinType },
5592
- ...raw.fields !== void 0 && { fields: raw.fields },
5593
- ...raw.date !== void 0 && { date: raw.date },
5594
- ...raw.title !== void 0 && { title: raw.title },
5595
- ...raw.html !== void 0 && { html: raw.html },
5596
- ...raw.pager !== void 0 && { pager: raw.pager },
5597
- ...device !== void 0 && { device },
5598
- ...raw.filterCond !== void 0 && { filterCond: raw.filterCond },
5599
- ...raw.sort !== void 0 && { sort: raw.sort }
5635
+ ...raw.builtinType !== void 0 ? { builtinType: raw.builtinType } : {},
5636
+ ...raw.fields !== void 0 ? { fields: raw.fields } : {},
5637
+ ...raw.date !== void 0 ? { date: raw.date } : {},
5638
+ ...raw.title !== void 0 ? { title: raw.title } : {},
5639
+ ...raw.html !== void 0 ? { html: raw.html } : {},
5640
+ ...raw.pager !== void 0 ? { pager: raw.pager } : {},
5641
+ ...device !== void 0 ? { device } : {},
5642
+ ...raw.filterCond !== void 0 ? { filterCond: raw.filterCond } : {},
5643
+ ...raw.sort !== void 0 ? { sort: raw.sort } : {}
5600
5644
  };
5601
5645
  }
5602
5646
  function toKintoneView(config) {
@@ -5636,9 +5680,7 @@ var KintoneViewConfigurator = class {
5636
5680
  revision: response.revision
5637
5681
  };
5638
5682
  } catch (error) {
5639
- if (isBusinessRuleError(error)) throw error;
5640
- if (error instanceof SystemError) throw error;
5641
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to get views", error);
5683
+ throw wrapKintoneError(error, "Failed to get views");
5642
5684
  }
5643
5685
  }
5644
5686
  async updateViews(params) {
@@ -5652,9 +5694,7 @@ var KintoneViewConfigurator = class {
5652
5694
  if (params.revision !== void 0) requestParams.revision = params.revision;
5653
5695
  return { revision: (await this.client.app.updateViews(requestParams)).revision };
5654
5696
  } catch (error) {
5655
- if (isBusinessRuleError(error)) throw error;
5656
- if (error instanceof SystemError) throw error;
5657
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to update views", error);
5697
+ throw wrapKintoneError(error, "Failed to update views");
5658
5698
  }
5659
5699
  }
5660
5700
  };
@@ -5770,7 +5810,7 @@ var KintoneSpaceReader = class {
5770
5810
  this.client = client;
5771
5811
  }
5772
5812
  async getSpaceApps(spaceId) {
5773
- if (!spaceId) throw new SystemError(SystemErrorCode.ExternalApiError, "spaceId must not be empty");
5813
+ if (!spaceId) throw new ValidationError(ValidationErrorCode.InvalidInput, "spaceId must not be empty");
5774
5814
  try {
5775
5815
  const rawApps = (await this.client.space.getSpace({ id: spaceId })).attachedApps;
5776
5816
  if (rawApps === void 0) throw new SystemError(SystemErrorCode.ExternalApiError, `Space API response for space ID ${spaceId} does not contain attachedApps. The kintone API response format may have changed.`);
@@ -5789,9 +5829,7 @@ var KintoneSpaceReader = class {
5789
5829
  }
5790
5830
  return result;
5791
5831
  } catch (error) {
5792
- if (isBusinessRuleError(error)) throw error;
5793
- if (error instanceof SystemError) throw error;
5794
- throw new SystemError(SystemErrorCode.ExternalApiError, `Failed to get space info for space ID: ${spaceId}`, error);
5832
+ throw wrapKintoneError(error, `Failed to get space info for space ID: ${spaceId}`);
5795
5833
  }
5796
5834
  }
5797
5835
  };
@@ -9217,15 +9255,19 @@ var KintoneFormDumpReader = class {
9217
9255
  }
9218
9256
  async getRawFormData() {
9219
9257
  try {
9220
- const [fields, layout] = await Promise.all([this.client.app.getFormFields({ app: this.appId }), this.client.app.getFormLayout({ app: this.appId })]);
9258
+ const [fields, layout] = await Promise.all([this.client.app.getFormFields({
9259
+ app: this.appId,
9260
+ preview: true
9261
+ }), this.client.app.getFormLayout({
9262
+ app: this.appId,
9263
+ preview: true
9264
+ })]);
9221
9265
  return {
9222
9266
  fields,
9223
9267
  layout
9224
9268
  };
9225
9269
  } catch (error) {
9226
- if (isBusinessRuleError(error)) throw error;
9227
- if (error instanceof SystemError) throw error;
9228
- throw new SystemError(SystemErrorCode.ExternalApiError, "Failed to fetch raw form data for dump", error);
9270
+ throw wrapKintoneError(error, "Failed to fetch raw form data for dump");
9229
9271
  }
9230
9272
  }
9231
9273
  };
@@ -9233,11 +9275,16 @@ var KintoneFormDumpReader = class {
9233
9275
  //#endregion
9234
9276
  //#region src/core/adapters/local/dumpStorage.ts
9235
9277
  var LocalFileDumpStorage = class {
9236
- constructor(filePrefix) {
9278
+ constructor(filePrefix, baseDir = process.cwd()) {
9237
9279
  this.filePrefix = filePrefix;
9280
+ this.baseDir = baseDir;
9281
+ const fieldsPath = resolve(this.baseDir, `${this.filePrefix}fields.json`);
9282
+ if (!isSafePath(fieldsPath, this.baseDir)) throw new ValidationError(ValidationErrorCode.InvalidInput, `Path traversal detected: "${fieldsPath}" escapes base directory "${this.baseDir}"`);
9283
+ const layoutPath = resolve(this.baseDir, `${this.filePrefix}layout.json`);
9284
+ if (!isSafePath(layoutPath, this.baseDir)) throw new ValidationError(ValidationErrorCode.InvalidInput, `Path traversal detected: "${layoutPath}" escapes base directory "${this.baseDir}"`);
9238
9285
  }
9239
9286
  async saveFields(content) {
9240
- const filePath = `${this.filePrefix}fields.json`;
9287
+ const filePath = resolve(this.baseDir, `${this.filePrefix}fields.json`);
9241
9288
  try {
9242
9289
  await mkdir(dirname(filePath), { recursive: true });
9243
9290
  await writeFile(filePath, content, "utf-8");
@@ -9246,7 +9293,7 @@ var LocalFileDumpStorage = class {
9246
9293
  }
9247
9294
  }
9248
9295
  async saveLayout(content) {
9249
- const filePath = `${this.filePrefix}layout.json`;
9296
+ const filePath = resolve(this.baseDir, `${this.filePrefix}layout.json`);
9250
9297
  try {
9251
9298
  await mkdir(dirname(filePath), { recursive: true });
9252
9299
  await writeFile(filePath, content, "utf-8");
@@ -9261,7 +9308,7 @@ var LocalFileDumpStorage = class {
9261
9308
  function createDumpCliContainer(config) {
9262
9309
  return {
9263
9310
  formDumpReader: new KintoneFormDumpReader(config.client ?? createKintoneClient(config), config.appId),
9264
- dumpStorage: new LocalFileDumpStorage(config.filePrefix)
9311
+ dumpStorage: new LocalFileDumpStorage(config.filePrefix, process.cwd())
9265
9312
  };
9266
9313
  }
9267
9314
 
@@ -9337,11 +9384,14 @@ var dump_default = define({
9337
9384
  function splitSubtableInnerFields(desired, current) {
9338
9385
  const newInnerFields = /* @__PURE__ */ new Map();
9339
9386
  const existingInnerFields = /* @__PURE__ */ new Map();
9387
+ const deletedInnerFieldCodes = [];
9340
9388
  for (const [code, def] of desired.properties.fields) if (current.properties.fields.has(code)) existingInnerFields.set(code, def);
9341
9389
  else newInnerFields.set(code, def);
9390
+ for (const code of current.properties.fields.keys()) if (!desired.properties.fields.has(code)) deletedInnerFieldCodes.push(code);
9342
9391
  return {
9343
9392
  newInnerFields,
9344
- existingInnerFields
9393
+ existingInnerFields,
9394
+ deletedInnerFieldCodes
9345
9395
  };
9346
9396
  }
9347
9397
 
@@ -9448,6 +9498,11 @@ function validateReferenceTableRelatedApp(field) {
9448
9498
  if (app.trim().length === 0) return [issue("error", field.code, field.type, "EMPTY_RELATED_APP", `Field "${field.code}" referenceTable.relatedApp.app must be non-empty`)];
9449
9499
  return [];
9450
9500
  }
9501
+ function validateSubtableHasInnerFields(field) {
9502
+ if (field.type !== "SUBTABLE") return [];
9503
+ if (field.properties.fields.size === 0) return [issue("error", field.code, field.type, "EMPTY_SUBTABLE", `Subtable "${field.code}" must have at least one inner field`)];
9504
+ return [];
9505
+ }
9451
9506
  const FIELD_VALIDATORS = [
9452
9507
  validateLabelNonEmpty,
9453
9508
  validateSelectionOptions,
@@ -9457,7 +9512,8 @@ const FIELD_VALIDATORS = [
9457
9512
  validateFileThumbnailSize,
9458
9513
  validateReferenceTableSize,
9459
9514
  validateLookupStructure,
9460
- validateReferenceTableRelatedApp
9515
+ validateReferenceTableRelatedApp,
9516
+ validateSubtableHasInnerFields
9461
9517
  ];
9462
9518
  function validateField(field) {
9463
9519
  return FIELD_VALIDATORS.flatMap((validator) => validator(field));
@@ -9506,6 +9562,7 @@ async function executeMigration({ container }) {
9506
9562
  const deleted = diff.entries.filter((e) => e.type === "deleted");
9507
9563
  const fieldsToAdd = [];
9508
9564
  const fieldsToUpdate = [];
9565
+ const innerFieldsToDelete = [];
9509
9566
  for (const entry of added) {
9510
9567
  if (entry.after === void 0) continue;
9511
9568
  if (subtableInnerCodes.has(entry.fieldCode)) continue;
@@ -9517,22 +9574,21 @@ async function executeMigration({ container }) {
9517
9574
  const after = entry.after;
9518
9575
  const before = entry.before;
9519
9576
  if (after.type === "SUBTABLE" && before !== void 0 && before.type === "SUBTABLE") {
9520
- const { newInnerFields, existingInnerFields } = splitSubtableInnerFields(after, before);
9521
- if (newInnerFields.size > 0) fieldsToAdd.push({
9522
- ...after,
9523
- properties: { fields: newInnerFields }
9524
- });
9577
+ const { newInnerFields, existingInnerFields, deletedInnerFieldCodes } = splitSubtableInnerFields(after, before);
9578
+ if (newInnerFields.size > 0) throw new ValidationError(ValidationErrorCode.InvalidInput, `kintone REST API does not support adding fields to an existing subtable. Use the schema override command instead. Subtable: ${after.code}`);
9525
9579
  if (existingInnerFields.size > 0) fieldsToUpdate.push({
9526
9580
  ...after,
9527
9581
  properties: { fields: existingInnerFields }
9528
9582
  });
9529
- } else fieldsToUpdate.push(after);
9583
+ for (const code of deletedInnerFieldCodes) innerFieldsToDelete.push(code);
9584
+ } else if (before !== void 0 && before.type !== after.type) throw new ValidationError(ValidationErrorCode.InvalidInput, `Field type change detected for "${after.code}" (${before.type} → ${after.type}). Use the schema override command instead.`);
9585
+ else fieldsToUpdate.push(after);
9530
9586
  }
9531
9587
  if (fieldsToAdd.length > 0) await container.formConfigurator.addFields(fieldsToAdd);
9532
9588
  if (fieldsToUpdate.length > 0) await container.formConfigurator.updateFields(fieldsToUpdate);
9533
- if (deleted.length > 0) {
9589
+ if (deleted.length > 0 || innerFieldsToDelete.length > 0) {
9534
9590
  const currentSubtableInnerCodes = collectSubtableInnerFieldCodes(currentFields);
9535
- const fieldCodes = deleted.filter((e) => !currentSubtableInnerCodes.has(e.fieldCode)).map((e) => e.fieldCode);
9591
+ const fieldCodes = [...deleted.filter((e) => !currentSubtableInnerCodes.has(e.fieldCode)).map((e) => e.fieldCode), ...innerFieldsToDelete];
9536
9592
  if (fieldCodes.length > 0) await container.formConfigurator.deleteFields(fieldCodes);
9537
9593
  }
9538
9594
  if (hasLayoutChanges) await container.formConfigurator.updateLayout(schema.layout);
@@ -9647,22 +9703,35 @@ async function forceOverrideForm({ container }) {
9647
9703
  const toAdd = [];
9648
9704
  const toUpdate = [];
9649
9705
  const toDelete = [];
9706
+ const innerFieldsToDelete = [];
9650
9707
  for (const [fieldCode, schemaDef] of schema.fields) {
9651
9708
  if (subtableInnerCodes.has(fieldCode)) continue;
9652
9709
  if (currentFields.has(fieldCode)) if (schemaDef.type === "SUBTABLE") {
9653
9710
  const currentDef = currentFields.get(fieldCode);
9654
9711
  if (currentDef !== void 0 && currentDef.type === "SUBTABLE") {
9655
- const { newInnerFields, existingInnerFields } = splitSubtableInnerFields(schemaDef, currentDef);
9656
- if (newInnerFields.size > 0) toAdd.push({
9657
- ...schemaDef,
9658
- properties: { fields: newInnerFields }
9659
- });
9660
- if (existingInnerFields.size > 0) toUpdate.push({
9661
- ...schemaDef,
9662
- properties: { fields: existingInnerFields }
9663
- });
9712
+ const { newInnerFields, existingInnerFields, deletedInnerFieldCodes } = splitSubtableInnerFields(schemaDef, currentDef);
9713
+ const allInnerFieldsRemoved = existingInnerFields.size === 0 && deletedInnerFieldCodes.length > 0;
9714
+ if (newInnerFields.size > 0 || allInnerFieldsRemoved) {
9715
+ toDelete.push(fieldCode);
9716
+ toAdd.push(schemaDef);
9717
+ } else {
9718
+ toUpdate.push({
9719
+ ...schemaDef,
9720
+ properties: { fields: existingInnerFields }
9721
+ });
9722
+ for (const code of deletedInnerFieldCodes) innerFieldsToDelete.push(code);
9723
+ }
9724
+ } else {
9725
+ toDelete.push(fieldCode);
9726
+ toAdd.push(schemaDef);
9727
+ }
9728
+ } else {
9729
+ const currentDef = currentFields.get(fieldCode);
9730
+ if (currentDef !== void 0 && currentDef.type !== schemaDef.type) {
9731
+ toDelete.push(fieldCode);
9732
+ toAdd.push(schemaDef);
9664
9733
  } else toUpdate.push(schemaDef);
9665
- } else toUpdate.push(schemaDef);
9734
+ }
9666
9735
  else toAdd.push(schemaDef);
9667
9736
  }
9668
9737
  const currentSubtableInnerCodes = collectSubtableInnerFieldCodes(currentFields);
@@ -9670,9 +9739,9 @@ async function forceOverrideForm({ container }) {
9670
9739
  if (currentSubtableInnerCodes.has(fieldCode)) continue;
9671
9740
  if (!schema.fields.has(fieldCode)) toDelete.push(fieldCode);
9672
9741
  }
9742
+ if (toDelete.length > 0 || innerFieldsToDelete.length > 0) await container.formConfigurator.deleteFields([...toDelete, ...innerFieldsToDelete]);
9673
9743
  if (toAdd.length > 0) await container.formConfigurator.addFields(toAdd);
9674
9744
  if (toUpdate.length > 0) await container.formConfigurator.updateFields(toUpdate);
9675
- if (toDelete.length > 0) await container.formConfigurator.deleteFields(toDelete);
9676
9745
  await container.formConfigurator.updateLayout(schema.layout);
9677
9746
  }
9678
9747