@arkebcacy/beacon-cli-temp 0.1.5 → 0.1.7

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 (69) hide show
  1. package/dist/cs/labels/Label.d.ts +8 -0
  2. package/dist/cs/labels/Label.d.ts.map +1 -0
  3. package/dist/cs/labels/Label.js +5 -0
  4. package/dist/cs/labels/Label.js.map +1 -0
  5. package/dist/cs/labels/getAllLabels.d.ts +2 -1
  6. package/dist/cs/labels/getAllLabels.d.ts.map +1 -1
  7. package/dist/cs/labels/getAllLabels.js +24 -4
  8. package/dist/cs/labels/getAllLabels.js.map +1 -1
  9. package/dist/cs/locales/addLocale.d.ts +11 -0
  10. package/dist/cs/locales/addLocale.d.ts.map +1 -0
  11. package/dist/cs/locales/addLocale.js +36 -0
  12. package/dist/cs/locales/addLocale.js.map +1 -0
  13. package/dist/cs/locales/ensureLocaleExists.d.ts +11 -0
  14. package/dist/cs/locales/ensureLocaleExists.d.ts.map +1 -0
  15. package/dist/cs/locales/ensureLocaleExists.js +38 -0
  16. package/dist/cs/locales/ensureLocaleExists.js.map +1 -0
  17. package/dist/cs/locales/getLocales.d.ts +12 -0
  18. package/dist/cs/locales/getLocales.d.ts.map +1 -0
  19. package/dist/cs/locales/getLocales.js +23 -0
  20. package/dist/cs/locales/getLocales.js.map +1 -0
  21. package/dist/dto/labels/NormalizedLabels.d.ts +13 -0
  22. package/dist/dto/labels/NormalizedLabels.d.ts.map +1 -0
  23. package/dist/dto/labels/NormalizedLabels.js +25 -0
  24. package/dist/dto/labels/NormalizedLabels.js.map +1 -0
  25. package/dist/dto/labels/flatten.d.ts +4 -0
  26. package/dist/dto/labels/flatten.d.ts.map +1 -0
  27. package/dist/dto/labels/flatten.js +18 -0
  28. package/dist/dto/labels/flatten.js.map +1 -0
  29. package/dist/dto/labels/organize.d.ts +4 -0
  30. package/dist/dto/labels/organize.d.ts.map +1 -0
  31. package/dist/dto/labels/organize.js +41 -0
  32. package/dist/dto/labels/organize.js.map +1 -0
  33. package/dist/schema/entries/toContentstack.d.ts.map +1 -1
  34. package/dist/schema/entries/toContentstack.js +30 -5
  35. package/dist/schema/entries/toContentstack.js.map +1 -1
  36. package/dist/schema/entries/toFilesystem.js +23 -10
  37. package/dist/schema/entries/toFilesystem.js.map +1 -1
  38. package/dist/schema/labels/lib/labelHelpers.d.ts +3 -0
  39. package/dist/schema/labels/lib/labelHelpers.d.ts.map +1 -0
  40. package/dist/schema/labels/lib/labelHelpers.js +34 -0
  41. package/dist/schema/labels/lib/labelHelpers.js.map +1 -0
  42. package/dist/schema/labels/lib/labelOperations.d.ts +6 -0
  43. package/dist/schema/labels/lib/labelOperations.d.ts.map +1 -0
  44. package/dist/schema/labels/lib/labelOperations.js +57 -0
  45. package/dist/schema/labels/lib/labelOperations.js.map +1 -0
  46. package/dist/schema/labels/toContentstack.d.ts.map +1 -1
  47. package/dist/schema/labels/toContentstack.js +145 -85
  48. package/dist/schema/labels/toContentstack.js.map +1 -1
  49. package/dist/schema/labels/toFilesystem.d.ts.map +1 -1
  50. package/dist/schema/labels/toFilesystem.js +10 -8
  51. package/dist/schema/labels/toFilesystem.js.map +1 -1
  52. package/dist/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +1 -1
  54. package/src/cs/labels/Label.ts +12 -0
  55. package/src/cs/labels/getAllLabels.ts +30 -5
  56. package/src/cs/locales/addLocale.ts +59 -0
  57. package/src/cs/locales/ensureLocaleExists.ts +51 -0
  58. package/src/cs/locales/getLocales.ts +38 -0
  59. package/src/dto/labels/NormalizedLabels.ts +47 -0
  60. package/src/dto/labels/flatten.test.ts +118 -0
  61. package/src/dto/labels/flatten.ts +27 -0
  62. package/src/dto/labels/organize.test.ts +131 -0
  63. package/src/dto/labels/organize.ts +58 -0
  64. package/src/schema/entries/toContentstack.ts +75 -6
  65. package/src/schema/entries/toFilesystem.ts +29 -14
  66. package/src/schema/labels/lib/labelHelpers.ts +36 -0
  67. package/src/schema/labels/lib/labelOperations.ts +74 -0
  68. package/src/schema/labels/toContentstack.ts +208 -91
  69. package/src/schema/labels/toFilesystem.ts +15 -9
@@ -3,6 +3,7 @@ import deleteEntry from '#cli/cs/entries/delete.js';
3
3
  import getEntryLocales from '#cli/cs/entries/getEntryLocales.js';
4
4
  import importEntry from '#cli/cs/entries/import.js';
5
5
  import type { Entry } from '#cli/cs/entries/Types.js';
6
+ import { ensureLocaleExists } from '#cli/cs/locales/ensureLocaleExists.js';
6
7
  import BeaconReplacer from '#cli/dto/entry/BeaconReplacer.js';
7
8
  import type ProgressBar from '#cli/ui/progress/ProgressBar.js';
8
9
  import type Ctx from '../ctx/Ctx.js';
@@ -45,7 +46,45 @@ export default async function toContentstack(
45
46
  update,
46
47
  });
47
48
 
48
- for (const title of result.unmodified) {
49
+ // Process unmodified entries to ensure all locale versions are synced
50
+ await processUnmodifiedEntries(
51
+ result.unmodified,
52
+ ctx,
53
+ contentType,
54
+ csEntriesByTitle,
55
+ fsEntriesByTitle,
56
+ transformer,
57
+ filenamesByTitle,
58
+ );
59
+
60
+ return result;
61
+ }
62
+
63
+ function shouldSyncLocales(
64
+ fsLocaleVersions: Awaited<ReturnType<typeof loadFsLocaleVersions>>,
65
+ csLocaleSet: Set<string>,
66
+ ): boolean {
67
+ // Check if we have new locale versions that don't exist in Contentstack
68
+ const fsLocaleSet = new Set(
69
+ fsLocaleVersions.map((lv) => (lv.locale === 'default' ? null : lv.locale)),
70
+ );
71
+ return Array.from(fsLocaleSet).some(
72
+ (locale) => locale !== null && !csLocaleSet.has(locale),
73
+ );
74
+ }
75
+
76
+ async function processUnmodifiedEntries(
77
+ unmodified: Iterable<string>,
78
+ ctx: Ctx,
79
+ contentType: ContentType,
80
+ csEntriesByTitle: ReadonlyMap<string, Entry>,
81
+ fsEntriesByTitle: ReadonlyMap<string, Entry>,
82
+ transformer: BeaconReplacer,
83
+ filenamesByTitle: ReadonlyMap<Entry['uid'], string>,
84
+ ) {
85
+ const ui = getUi();
86
+
87
+ for (const title of unmodified) {
49
88
  const cs = csEntriesByTitle.get(title);
50
89
  const fs = fsEntriesByTitle.get(title);
51
90
 
@@ -60,11 +99,33 @@ export default async function toContentstack(
60
99
  throw new Error(`No matching entry found for ${title}.`);
61
100
  }
62
101
 
102
+ // Load all locale versions from filesystem
103
+ const fsLocaleVersions = await loadFsLocaleVersions(
104
+ fs,
105
+ contentType.uid,
106
+ filenamesByTitle,
107
+ );
108
+
109
+ const csLocaleSet = await getExistingLocales(ctx, contentType, cs.uid);
110
+
111
+ // Only push if there are new locale versions to sync
112
+ if (!shouldSyncLocales(fsLocaleVersions, csLocaleSet)) {
113
+ continue;
114
+ }
115
+
116
+ // Push all locale versions (including new locales like zh-cn)
117
+ await updateAllLocales(
118
+ ctx,
119
+ transformer,
120
+ contentType,
121
+ fsLocaleVersions,
122
+ cs.uid,
123
+ csLocaleSet,
124
+ );
125
+
63
126
  const entry = { ...fs, uid: cs.uid };
64
127
  ctx.references.recordEntryForReferences(contentType.uid, entry);
65
128
  }
66
-
67
- return result;
68
129
  }
69
130
 
70
131
  function buildUpdateFn(
@@ -141,6 +202,13 @@ async function updateAllLocales(
141
202
  entryUid: string,
142
203
  csLocaleSet: Set<string>,
143
204
  ) {
205
+ // Ensure all required locales exist in the target stack before pushing entries
206
+ const localeEnsurePromises = fsLocaleVersions
207
+ .filter((lv) => lv.locale !== 'default')
208
+ .map(async (lv) => ensureLocaleExists(ctx.cs.client, lv.locale));
209
+
210
+ await Promise.all(localeEnsurePromises);
211
+
144
212
  // Import all locale versions in parallel for better performance
145
213
  const importPromises = fsLocaleVersions.map(async (localeVersion) => {
146
214
  const transformed = transformer.process(localeVersion.entry);
@@ -148,9 +216,10 @@ async function updateAllLocales(
148
216
  // Pass undefined for 'default' locale (single-locale backward compat)
149
217
  const locale =
150
218
  localeVersion.locale === 'default' ? undefined : localeVersion.locale;
151
- const overwrite = locale
152
- ? csLocaleSet.has(localeVersion.locale)
153
- : csLocaleSet.size > 0;
219
+
220
+ // Always use overwrite=true for locale-specific versions since the entry exists.
221
+ // For 'default' locale (single-locale backward compat), only overwrite if entry has locales.
222
+ const overwrite = locale ? true : csLocaleSet.size > 0;
154
223
 
155
224
  return importEntry(
156
225
  ctx.cs.client,
@@ -80,28 +80,24 @@ function createWriteFn(
80
80
  }
81
81
 
82
82
  // If only one locale, save without locale suffix for backward compatibility.
83
- // When multiple locales exist prefer writing a single base file when an
84
- // English locale is present (fixtures expect base filenames like
85
- // `Autumn Feast and Social.yaml`). Only use locale suffix when no
86
- // English locale is available.
87
- const hasEnglish = locales.some((l) => /^en(?:[-_]|$)/iu.test(l.code));
88
- const useLocaleSuffix = locales.length > 1 && !hasEnglish;
89
-
90
- // Log locale details for debugging why specific locales (e.g. Chinese)
91
- // may not be written to the filesystem.
92
- // Debug logging removed to avoid unsafe-call lint errors.
83
+ // When multiple locales exist, write English as base file and other locales
84
+ // with locale suffixes (e.g., Entry.yaml for English, Entry.zh-chs.yaml for Chinese)
93
85
 
94
86
  // Write all locale versions in parallel for better performance
95
- const writePromises = locales.map(async (locale) =>
96
- writeLocaleVersion(
87
+ const writePromises = locales.map(async (locale) => {
88
+ const isEnglish = /^en(?:[-_]|$)/iu.test(locale.code);
89
+ // Use locale suffix for non-English locales when multiple locales exist
90
+ const useLocaleSuffix = locales.length > 1 && !isEnglish;
91
+
92
+ return writeLocaleVersion(
97
93
  ctx,
98
94
  contentType,
99
95
  entry,
100
96
  locale.code,
101
97
  getBasePath,
102
98
  useLocaleSuffix,
103
- ),
104
- );
99
+ );
100
+ });
105
101
 
106
102
  await Promise.all(writePromises);
107
103
  };
@@ -122,8 +118,27 @@ async function writeLocaleVersion(
122
118
  localeCode,
123
119
  );
124
120
 
121
+ // Skip writing this locale version if it's a fallback (locale doesn't match requested)
122
+ // This happens when Contentstack returns the default locale content because
123
+ // no localized version exists for the requested locale
124
+ if (
125
+ useLocaleSuffix &&
126
+ exported.locale &&
127
+ typeof exported.locale === 'string' &&
128
+ exported.locale !== localeCode
129
+ ) {
130
+ // This is a fallback locale, skip writing it
131
+ return;
132
+ }
133
+
125
134
  const { uid, ...transformed } = transformEntry(ctx, contentType, exported);
126
135
 
136
+ // Preserve the actual locale code from the source stack
137
+ // This ensures we maintain the exact language-region configuration
138
+ if ('locale' in transformed) {
139
+ transformed.locale = localeCode;
140
+ }
141
+
127
142
  const basePath = getBasePath(entry);
128
143
  const filePath = useLocaleSuffix
129
144
  ? basePath.replace(/\.yaml$/u, `.${localeCode}.yaml`)
@@ -0,0 +1,36 @@
1
+ import isRecord from '#cli/util/isRecord.js';
2
+
3
+ export function canonicalize(value: unknown): unknown {
4
+ if (Array.isArray(value)) {
5
+ const result: unknown[] = [];
6
+ for (const item of value) {
7
+ result.push(canonicalize(item));
8
+ }
9
+ return result;
10
+ }
11
+ if (isRecord(value)) {
12
+ const obj: Record<string, unknown> = value;
13
+ const out: Record<string, unknown> = {};
14
+ const objKeys: string[] = Object.keys(obj);
15
+ objKeys.sort();
16
+ for (const key of objKeys) {
17
+ if (key === 'uid' || key === 'created_at' || key === 'updated_at')
18
+ continue;
19
+ // Normalize parent: skip if empty array
20
+ const val: unknown = obj[key];
21
+ if (key === 'parent' && Array.isArray(val) && val.length === 0) continue;
22
+ const canonicalizedVal: unknown = canonicalize(val);
23
+ out[key] = canonicalizedVal;
24
+ }
25
+ return out;
26
+ }
27
+ return value;
28
+ }
29
+
30
+ export function prepareLabel(
31
+ label: Record<string, unknown>,
32
+ ): Record<string, unknown> {
33
+ // Labels are sent to the API with all fields intact
34
+ // The API expects the parent field as an array
35
+ return label;
36
+ }
@@ -0,0 +1,74 @@
1
+ import ContentstackError from '#cli/cs/api/ContentstackError.js';
2
+ import isRecord from '#cli/util/isRecord.js';
3
+ import type Ctx from '../../ctx/Ctx.js';
4
+ import type { MutableTransferResults } from '../../xfer/TransferResults.js';
5
+ import type Label from '#cli/cs/labels/Label.js';
6
+ import { canonicalize, prepareLabel } from './labelHelpers.js';
7
+
8
+ export async function updateIfNecessary(
9
+ ctx: Ctx,
10
+ uid: string,
11
+ localLabel: Record<string, unknown>,
12
+ remoteLabel: Label | undefined,
13
+ results: MutableTransferResults,
14
+ ) {
15
+ if (!remoteLabel) {
16
+ // If we don't have remote data, err on the side of updating
17
+ const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
18
+ body: { label: prepareLabel(localLabel) },
19
+ params: { path: { label_uid: uid } },
20
+ })) as unknown;
21
+ const putError = (res as { error?: unknown } | undefined)?.error;
22
+ ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
23
+ results.updated.add(uid);
24
+ return;
25
+ }
26
+
27
+ let shouldUpdate = true;
28
+ try {
29
+ shouldUpdate =
30
+ JSON.stringify(canonicalize(localLabel)) !==
31
+ JSON.stringify(canonicalize(remoteLabel));
32
+ } catch {
33
+ shouldUpdate = true;
34
+ }
35
+
36
+ if (!shouldUpdate) return;
37
+
38
+ const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
39
+ body: { label: prepareLabel(localLabel) },
40
+ params: { path: { label_uid: uid } },
41
+ })) as unknown;
42
+ const putError = (res as { error?: unknown } | undefined)?.error;
43
+ ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
44
+ results.updated.add(uid);
45
+ }
46
+
47
+ export async function createLabel(
48
+ ctx: Ctx,
49
+ localLabel: Record<string, unknown>,
50
+ results: MutableTransferResults,
51
+ ): Promise<string | null> {
52
+ const res = (await ctx.cs.client.POST('/v3/labels', {
53
+ body: { label: prepareLabel(localLabel) },
54
+ })) as unknown;
55
+
56
+ const postError = (res as { error?: unknown } | undefined)?.error;
57
+ const labelName = typeof localLabel.name === 'string' ? localLabel.name : '';
58
+ ContentstackError.throwIfError(
59
+ postError,
60
+ `Failed to create label: ${labelName}`,
61
+ );
62
+ const postData = (res as { data?: unknown } | undefined)?.data;
63
+ let createdUid: string | null = null;
64
+ if (isRecord(postData)) {
65
+ const pd = postData;
66
+ if (isRecord(pd.label)) {
67
+ const labelObj = pd.label;
68
+ if (typeof labelObj.uid === 'string') createdUid = labelObj.uid;
69
+ }
70
+ if (createdUid === null && typeof pd.uid === 'string') createdUid = pd.uid;
71
+ }
72
+ results.created.add(createdUid ?? '<created>');
73
+ return createdUid;
74
+ }
@@ -1,139 +1,256 @@
1
1
  import readYaml from '#cli/fs/readYaml.js';
2
- import schemaDirectory from '../content-types/schemaDirectory.js';
3
- import ContentstackError from '#cli/cs/api/ContentstackError.js';
4
2
  import type Ctx from '../ctx/Ctx.js';
5
3
  import { MutableTransferResults } from '../xfer/TransferResults.js';
6
4
  import getUi from '../lib/SchemaUi.js';
7
5
  import isRecord from '#cli/util/isRecord.js';
6
+ import flatten from '#cli/dto/labels/flatten.js';
7
+ import { isNormalizedLabels } from '#cli/dto/labels/NormalizedLabels.js';
8
+ import type Label from '#cli/cs/labels/Label.js';
9
+ import getAllLabels from '#cli/cs/labels/getAllLabels.js';
10
+ import { createLabel, updateIfNecessary } from './lib/labelOperations.js';
11
+ import { resolve } from 'node:path';
8
12
 
9
- export default async function toContentstack(ctx: Ctx) {
10
- const directory = schemaDirectory();
11
- const path = `${directory}/labels.yaml`;
13
+ /**
14
+ * Sort labels to ensure parents are processed before children.
15
+ * Labels without parents come first, then labels are sorted by dependency depth.
16
+ */
17
+ function sortLabelsByDependency(labels: Label[]): Label[] {
18
+ const labelsByUid = new Map<string, Label>();
19
+ const sorted: Label[] = [];
20
+ const processed = new Set<string>();
21
+
22
+ // Build a map for quick lookup
23
+ for (const label of labels) {
24
+ if (isRecord(label) && typeof label.uid === 'string') {
25
+ labelsByUid.set(label.uid, label as Label);
26
+ }
27
+ }
12
28
 
29
+ // Recursive function to add a label and its dependencies
30
+ function addLabel(label: Label) {
31
+ const uid = typeof label.uid === 'string' ? label.uid : '';
32
+ if (!uid || processed.has(uid)) return;
33
+
34
+ // First, process parent if it exists in our label set
35
+ const parentArray = label.parent;
36
+ if (Array.isArray(parentArray) && parentArray.length > 0) {
37
+ // Use destructuring to satisfy eslint prefer-destructuring rule
38
+ const [parentValue] = parentArray as unknown as readonly [
39
+ string,
40
+ ...string[],
41
+ ];
42
+ if (labelsByUid.has(parentValue) && !processed.has(parentValue)) {
43
+ const parentLabel = labelsByUid.get(parentValue);
44
+ if (parentLabel) addLabel(parentLabel);
45
+ }
46
+ }
47
+
48
+ // Now add this label
49
+ sorted.push(label);
50
+ processed.add(uid);
51
+ }
52
+
53
+ // Process all labels
54
+ for (const label of labels) {
55
+ if (isRecord(label)) {
56
+ addLabel(label as Label);
57
+ }
58
+ }
59
+
60
+ return sorted;
61
+ }
62
+
63
+ export default async function toContentstack(ctx: Ctx) {
13
64
  const ui = getUi();
65
+ const path = resolve(ui.options.schema.schemaPath, 'labels.yaml');
14
66
 
15
- let data: unknown;
16
- try {
17
- data = await readYaml(path);
18
- } catch {
19
- ui.info(`Labels: no file at ${path}, skipping`);
67
+ const data = await loadLabelsData(path, ui);
68
+ if (!data) {
20
69
  return new MutableTransferResults();
21
70
  }
22
71
 
23
- const labels =
24
- isRecord(data) && Array.isArray(data.labels)
25
- ? data.labels
26
- : Array.isArray(data)
27
- ? (data as unknown[])
28
- : [];
72
+ const flatLabels = extractFlatLabels(data);
73
+ ui.info(`Labels: read ${flatLabels.length} label(s) from ${path}`);
29
74
 
30
- ui.info(`Labels: read ${labels.length} label(s) from ${path}`);
75
+ const {
76
+ localUidToName,
77
+ nameToRemoteUid,
78
+ remoteLabelsByName,
79
+ remoteLabelsByUid,
80
+ } = await prepareLabelMappings(ctx, flatLabels);
31
81
 
82
+ const sortedLabels = sortLabelsByDependency(flatLabels);
32
83
  const results = new MutableTransferResults();
33
84
 
34
- for (const labelRaw of labels) {
85
+ for (const labelRaw of sortedLabels) {
35
86
  if (!isRecord(labelRaw)) continue;
36
- // keep per-label logic in helper to reduce complexity of this function
37
87
 
38
- await handleLabel(ctx, labelRaw, results);
88
+ await handleLabel(
89
+ ctx,
90
+ labelRaw,
91
+ remoteLabelsByUid,
92
+ remoteLabelsByName,
93
+ nameToRemoteUid,
94
+ localUidToName,
95
+ results,
96
+ );
39
97
  }
40
98
 
41
99
  return results;
42
100
  }
43
101
 
44
- function canonicalize(value: unknown): unknown {
45
- if (Array.isArray(value)) return value.map(canonicalize);
46
- if (isRecord(value)) {
47
- const obj = value;
48
- const out: Record<string, unknown> = {};
49
- for (const key of Object.keys(obj).sort()) {
50
- if (key === 'uid' || key === 'created_at' || key === 'updated_at')
51
- continue;
52
- out[key] = canonicalize(obj[key]);
53
- }
54
- return out;
102
+ async function loadLabelsData(
103
+ path: string,
104
+ ui: ReturnType<typeof getUi>,
105
+ ): Promise<unknown> {
106
+ try {
107
+ return await readYaml(path);
108
+ } catch {
109
+ ui.info(`Labels: no file at ${path}, skipping`);
110
+ return undefined;
55
111
  }
56
- return value;
57
112
  }
58
113
 
59
- async function updateIfNecessary(
60
- ctx: Ctx,
61
- uid: string,
62
- localLabel: Record<string, unknown>,
63
- results: MutableTransferResults,
64
- ) {
65
- const remoteRes = (await ctx.cs.client.GET('/v3/labels/{label_uid}', {
66
- params: { path: { label_uid: uid } },
67
- })) as unknown;
68
- const remoteData = (remoteRes as { data?: unknown } | undefined)?.data;
69
-
70
- if (!isRecord(remoteData) || !isRecord(remoteData.label)) {
71
- // If we can't parse remote data, err on the side of updating
72
- const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
73
- body: { label: localLabel },
74
- params: { path: { label_uid: uid } },
75
- })) as unknown;
76
- const putError = (res as { error?: unknown } | undefined)?.error;
77
- ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
78
- results.updated.add(uid);
79
- return;
114
+ function extractFlatLabels(data: unknown): Label[] {
115
+ if (isNormalizedLabels(data)) {
116
+ interface LabelWithChildren {
117
+ children?: unknown;
118
+ }
119
+ const hasChildren = (data.labels as unknown as LabelWithChildren[]).some(
120
+ (l) => 'children' in l && l.children !== undefined,
121
+ );
122
+
123
+ if (hasChildren) {
124
+ return [...flatten(data.labels)];
125
+ }
126
+ return [...data.labels] as unknown as Label[];
80
127
  }
81
128
 
82
- const remoteLabel = remoteData.label;
83
- let shouldUpdate = true;
84
- try {
85
- shouldUpdate =
86
- JSON.stringify(canonicalize(localLabel)) !==
87
- JSON.stringify(canonicalize(remoteLabel));
88
- } catch {
89
- shouldUpdate = true;
129
+ if (isRecord(data) && Array.isArray(data.labels)) {
130
+ return data.labels as Label[];
90
131
  }
91
132
 
92
- if (!shouldUpdate) return;
133
+ if (Array.isArray(data)) {
134
+ return data as Label[];
135
+ }
93
136
 
94
- const res = (await ctx.cs.client.PUT('/v3/labels/{label_uid}', {
95
- body: { label: localLabel },
96
- params: { path: { label_uid: uid } },
97
- })) as unknown;
98
- const putError = (res as { error?: unknown } | undefined)?.error;
99
- ContentstackError.throwIfError(putError, `Failed to update label: ${uid}`);
100
- results.updated.add(uid);
137
+ return [];
101
138
  }
102
139
 
103
- async function createLabel(
104
- ctx: Ctx,
105
- localLabel: Record<string, unknown>,
106
- results: MutableTransferResults,
107
- ) {
108
- const res = (await ctx.cs.client.POST('/v3/labels', {
109
- body: { label: localLabel },
110
- })) as unknown;
111
- const postError = (res as { error?: unknown } | undefined)?.error;
112
- ContentstackError.throwIfError(postError, `Failed to create label`);
113
- const postData = (res as { data?: unknown } | undefined)?.data;
114
- let createdUid: string | null = null;
115
- if (isRecord(postData)) {
116
- const pd = postData;
117
- if (isRecord(pd.label)) {
118
- const labelObj = pd.label;
119
- if (typeof labelObj.uid === 'string') createdUid = labelObj.uid;
140
+ async function prepareLabelMappings(ctx: Ctx, flatLabels: Label[]) {
141
+ const remoteLabels = await getAllLabels(ctx.cs.client);
142
+ const remoteLabelsByUid = new Map<string, Label>();
143
+ const remoteLabelsByName = new Map<string, Label>();
144
+
145
+ for (const label of remoteLabels) {
146
+ remoteLabelsByUid.set(label.uid, label);
147
+ remoteLabelsByName.set(label.name, label);
148
+ }
149
+
150
+ const nameToRemoteUid = new Map<string, string>();
151
+ for (const [name, label] of remoteLabelsByName) {
152
+ nameToRemoteUid.set(name, label.uid);
153
+ }
154
+
155
+ const localUidToName = new Map<string, string>();
156
+ for (const label of flatLabels) {
157
+ if (
158
+ isRecord(label) &&
159
+ typeof label.uid === 'string' &&
160
+ typeof label.name === 'string'
161
+ ) {
162
+ localUidToName.set(label.uid, label.name);
120
163
  }
121
- if (createdUid === null && typeof pd.uid === 'string') createdUid = pd.uid;
122
164
  }
123
- results.created.add(createdUid ?? '<created>');
165
+
166
+ return {
167
+ localUidToName,
168
+ nameToRemoteUid,
169
+ remoteLabelsByName,
170
+ remoteLabelsByUid,
171
+ };
124
172
  }
125
173
 
126
174
  async function handleLabel(
127
175
  ctx: Ctx,
128
176
  labelRaw: Record<string, unknown>,
177
+ remoteLabelsByUid: Map<string, Label>,
178
+ remoteLabelsByName: Map<string, Label>,
179
+ nameToRemoteUid: Map<string, string>,
180
+ localUidToName: Map<string, string>,
129
181
  results: MutableTransferResults,
130
182
  ) {
131
183
  const uid = typeof labelRaw.uid === 'string' ? labelRaw.uid : '';
184
+ const name = typeof labelRaw.name === 'string' ? labelRaw.name : '';
185
+
186
+ const preparedLabel = translateParentReference(
187
+ labelRaw,
188
+ localUidToName,
189
+ nameToRemoteUid,
190
+ );
191
+
192
+ // First try to find by UID (exact match from same stack)
193
+ if (uid.length > 0 && remoteLabelsByUid.has(uid)) {
194
+ const remoteLabel = remoteLabelsByUid.get(uid);
195
+ await updateIfNecessary(ctx, uid, preparedLabel, remoteLabel, results);
196
+ return;
197
+ }
132
198
 
133
- if (uid.length) {
134
- await updateIfNecessary(ctx, uid, labelRaw, results);
199
+ // Then try to find by name (for cross-stack scenarios)
200
+ if (name.length > 0 && remoteLabelsByName.has(name)) {
201
+ const remoteLabel = remoteLabelsByName.get(name);
202
+ if (!remoteLabel) {
203
+ return;
204
+ }
205
+ await updateIfNecessary(
206
+ ctx,
207
+ remoteLabel.uid,
208
+ preparedLabel,
209
+ remoteLabel,
210
+ results,
211
+ );
212
+ nameToRemoteUid.set(name, remoteLabel.uid);
135
213
  return;
136
214
  }
137
215
 
138
- await createLabel(ctx, labelRaw, results);
216
+ // Label doesn't exist remotely, create it
217
+ const createdUid = await createLabel(ctx, preparedLabel, results);
218
+
219
+ if (createdUid && name) {
220
+ nameToRemoteUid.set(name, createdUid);
221
+ }
222
+ }
223
+
224
+ function translateParentReference(
225
+ labelRaw: Record<string, unknown>,
226
+ localUidToName: Map<string, string>,
227
+ nameToRemoteUid: Map<string, string>,
228
+ ): Record<string, unknown> {
229
+ const parentField = labelRaw.parent;
230
+ if (!Array.isArray(parentField) || parentField.length === 0) {
231
+ return { ...labelRaw };
232
+ }
233
+
234
+ // TypeScript knows parentField is an array, but we need to type it properly
235
+ const [firstElement] = parentField as unknown[];
236
+ if (typeof firstElement !== 'string') {
237
+ return { ...labelRaw };
238
+ }
239
+ const firstParent: string = firstElement;
240
+
241
+ const parentName = localUidToName.get(firstParent);
242
+
243
+ if (!parentName || !nameToRemoteUid.has(parentName)) {
244
+ return { ...labelRaw };
245
+ }
246
+
247
+ const remoteParentUid = nameToRemoteUid.get(parentName);
248
+ if (!remoteParentUid) {
249
+ return { ...labelRaw };
250
+ }
251
+
252
+ return {
253
+ ...labelRaw,
254
+ parent: [remoteParentUid],
255
+ };
139
256
  }
@@ -1,24 +1,30 @@
1
1
  import getAllLabels from '#cli/cs/labels/getAllLabels.js';
2
2
  import writeYaml from '#cli/fs/writeYaml.js';
3
- import schemaDirectory from '../content-types/schemaDirectory.js';
3
+ import organize from '#cli/dto/labels/organize.js';
4
4
  import { MutableTransferResults } from '../xfer/TransferResults.js';
5
5
  import createProgressBar from '../lib/createProgressBar.js';
6
6
  import type Ctx from '../ctx/Ctx.js';
7
+ import getUi from '../lib/SchemaUi.js';
8
+ import { resolve } from 'node:path';
7
9
 
8
10
  export default async function toFilesystem(ctx: Ctx) {
9
- const directory = schemaDirectory();
11
+ const ui = getUi();
12
+ const {
13
+ options: {
14
+ schema: { schemaPath },
15
+ },
16
+ } = ui;
17
+ const labelsPath = resolve(schemaPath, 'labels.yaml');
10
18
  const bar = createProgressBar('Labels', 1, 0);
11
19
 
12
- // Fetch labels and write them directly. processPlan shortcuts when the
13
- // merge plan is empty, so for a single-file resource we write and return
14
- // a simple TransferResults object.
15
- const labels = await getAllLabels(ctx.cs.client);
16
- await writeYaml(`${directory}/labels.yaml`, { labels });
20
+ // Fetch labels and organize them into a hierarchical structure
21
+ const flatLabels = await getAllLabels(ctx.cs.client);
22
+ const hierarchicalLabels = organize(flatLabels);
23
+
24
+ await writeYaml(labelsPath, { labels: hierarchicalLabels });
17
25
 
18
26
  const result = new MutableTransferResults();
19
27
  result.created.add('labels.yaml');
20
28
  bar.increment();
21
29
  return result;
22
30
  }
23
-
24
- //# sourceMappingURL=toFilesystem.js.map