sb-mig 6.2.0 → 6.3.0-beta.2

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 (44) hide show
  1. package/README.md +15 -4
  2. package/dist/api/assets/asset-folders.d.ts +3 -0
  3. package/dist/api/assets/asset-folders.js +56 -0
  4. package/dist/api/assets/asset-folders.types.d.ts +29 -0
  5. package/dist/api/assets/asset-folders.types.js +1 -0
  6. package/dist/api/assets/assets.d.ts +4 -1
  7. package/dist/api/assets/assets.js +61 -16
  8. package/dist/api/assets/assets.types.d.ts +12 -1
  9. package/dist/api/assets/index.d.ts +4 -2
  10. package/dist/api/assets/index.js +2 -1
  11. package/dist/api/copy/assets.d.ts +12 -0
  12. package/dist/api/copy/assets.js +84 -0
  13. package/dist/api/copy/graph.d.ts +8 -0
  14. package/dist/api/copy/graph.js +27 -0
  15. package/dist/api/copy/index.d.ts +6 -0
  16. package/dist/api/copy/index.js +6 -0
  17. package/dist/api/copy/manifest.d.ts +23 -0
  18. package/dist/api/copy/manifest.js +115 -0
  19. package/dist/api/copy/reference-rewriter.d.ts +6 -0
  20. package/dist/api/copy/reference-rewriter.js +153 -0
  21. package/dist/api/copy/reference-scanner.d.ts +23 -0
  22. package/dist/api/copy/reference-scanner.js +322 -0
  23. package/dist/api/copy/types.d.ts +196 -0
  24. package/dist/api/copy/types.js +1 -0
  25. package/dist/api/data-migration/component-data-migration.d.ts +81 -0
  26. package/dist/api/data-migration/component-data-migration.js +391 -95
  27. package/dist/api/data-migration/migration-run-log.d.ts +3 -1
  28. package/dist/api/data-migration/migration-run-log.js +2 -1
  29. package/dist/api/managementApi.d.ts +6 -0
  30. package/dist/api/stories/stories.js +3 -3
  31. package/dist/api/stories/stories.types.d.ts +3 -1
  32. package/dist/api/testApi.d.ts +6 -0
  33. package/dist/api-v2/requestConfig.d.ts +17 -1
  34. package/dist/api-v2/requestConfig.js +28 -0
  35. package/dist/cli/cli-descriptions.d.ts +3 -3
  36. package/dist/cli/cli-descriptions.js +92 -18
  37. package/dist/cli/commands/copy.js +2091 -145
  38. package/dist/cli/commands/migrate.js +27 -1
  39. package/dist/cli/index.js +3 -0
  40. package/dist/utils/files.d.ts +2 -0
  41. package/dist/utils/files.js +86 -3
  42. package/dist-cjs/api/stories/stories.js +3 -3
  43. package/dist-cjs/api-v2/requestConfig.js +29 -0
  44. package/package.json +2 -2
package/README.md CHANGED
@@ -9,13 +9,20 @@ If you've found an issue or you have feature request - <a href="https://github.c
9
9
  [![GitHub issues](https://img.shields.io/github/issues/sb-mig/sb-mig.svg?style=flat-square&v=1)](https://github.com/sb-mig/sb-mig/issues?q=is%3Aopen+is%3Aissue)
10
10
  ![npm](https://img.shields.io/npms-io/maintenance-score/sb-mig)
11
11
 
12
+ # Documentation
13
+
14
+ - [Docs site](https://sb-mig.vercel.app) is the canonical documentation for current sb-mig workflows.
15
+ - [Quickstart](https://sb-mig.vercel.app/docs/quickstart) covers installation, credentials, and first safe commands.
16
+ - [CLI reference](https://sb-mig.vercel.app/docs/reference/cli) covers command families and safety conventions.
17
+ - [LLM docs index](https://sb-mig.vercel.app/llms.txt) and [full Markdown corpus](https://sb-mig.vercel.app/llms-full.txt) are available for coding agents and other LLM tools.
18
+
12
19
  # Requirements:
13
20
 
14
21
  | | |
15
22
  | ---- | ------------ |
16
23
  | Node | 22.x.x or >=24.x.x |
17
24
 
18
- # 6.x.x version released!
25
+ # Current release notes: 6.x.x
19
26
 
20
27
  ## Important Updates
21
28
 
@@ -38,7 +45,11 @@ If you've found an issue or you have feature request - <a href="https://github.c
38
45
  - `--publishLanguages` is now `--publicationLanguages`
39
46
  - `--preservePublishedLayer` is now `--publicationMode preserve-layers`
40
47
 
41
- # 5.x.x version released!
48
+ # Historical release notes
49
+
50
+ The sections below are retained for historical context. For current usage, prefer the [docs site](https://sb-mig.vercel.app) and `sb-mig --help`.
51
+
52
+ ## 5.x.x version released!
42
53
 
43
54
  ## Important Updates
44
55
 
@@ -60,13 +71,13 @@ If you've found an issue or you have feature request - <a href="https://github.c
60
71
 
61
72
  Do not hesitate to get in touch if you encounter any issues or require further clarification on any points.
62
73
 
63
- # 4.x.x version released!
74
+ ## 4.x.x version released!
64
75
 
65
76
  - Whole deployment now, is handled by [semantic-release](https://github.com/semantic-release/semantic-release). And is just normal repository, instead of Lerna monorepo which is not needed anymore, and it was recently unmaintained (now it was passed to `nrwl` to maintain (https://github.com/lerna/lerna/issues/3121) will see what will happen in future with it :)
66
77
  - Fromt the code perspective, there are no **breaking changes** between **3.x.x** and **4.x.x** but i'm going to fix some stuff now and add some more functionalities to it. So stay tuned!
67
78
  - This Readme, which is also kinda documentation, will also be updated. Cause in some places there is misleading information. The best documentation though is just `sb-mig --help` command.
68
79
 
69
- # 3.x.x version released!
80
+ ## 3.x.x version released!
70
81
 
71
82
  - completely rewritten to simple [Meow](https://github.com/sindresorhus/meow) lib with help of Typescript. Check [migration guide](https://github.com/sb-mig/sb-mig/blob/oclif-research/MIGRATION-GUIDE-v3.md)
72
83
  - support native es modules
@@ -0,0 +1,3 @@
1
+ import type { CreateAssetFolder, GetAllAssetFolders } from "./asset-folders.types.js";
2
+ export declare const getAllAssetFolders: GetAllAssetFolders;
3
+ export declare const createAssetFolder: CreateAssetFolder;
@@ -0,0 +1,56 @@
1
+ import Logger from "../../utils/logger.js";
2
+ import { getAllItemsWithPagination } from "../utils/request.js";
3
+ const joinQueryValues = (values) => values && values.length > 0 ? values.join(",") : undefined;
4
+ export const getAllAssetFolders = async (args, config) => {
5
+ const { spaceId, search, withParent, byIds, byUuids } = args;
6
+ const { sbApi } = config;
7
+ const assetFolders = await getAllItemsWithPagination({
8
+ apiFn: ({ per_page, page }) => sbApi
9
+ .get(`spaces/${spaceId}/asset_folders/`, {
10
+ ...(search ? { search } : {}),
11
+ ...(withParent !== undefined && withParent !== null
12
+ ? { with_parent: String(withParent) }
13
+ : {}),
14
+ ...(joinQueryValues(byIds)
15
+ ? { by_ids: joinQueryValues(byIds) }
16
+ : {}),
17
+ ...(joinQueryValues(byUuids)
18
+ ? { by_uuids: joinQueryValues(byUuids) }
19
+ : {}),
20
+ per_page,
21
+ page,
22
+ })
23
+ .catch((err) => {
24
+ if (err.response?.status === 404) {
25
+ Logger.error(`There are no asset folders in your Storyblok ${spaceId} space.`);
26
+ return {
27
+ data: { asset_folders: [] },
28
+ total: 0,
29
+ perPage: 100,
30
+ };
31
+ }
32
+ Logger.error(err);
33
+ throw err;
34
+ }),
35
+ params: {},
36
+ itemsKey: "asset_folders",
37
+ });
38
+ return { asset_folders: assetFolders };
39
+ };
40
+ export const createAssetFolder = async ({ spaceId, payload }, config) => {
41
+ const { sbApi } = config;
42
+ return sbApi
43
+ .post(`spaces/${spaceId}/asset_folders/`, {
44
+ asset_folder: {
45
+ name: payload.name,
46
+ ...(payload.parent_id === undefined
47
+ ? {}
48
+ : { parent_id: payload.parent_id }),
49
+ },
50
+ })
51
+ .then(({ data }) => data)
52
+ .catch((err) => {
53
+ Logger.error(err);
54
+ throw err;
55
+ });
56
+ };
@@ -0,0 +1,29 @@
1
+ import type { RequestBaseConfig } from "../utils/request.js";
2
+ export interface SBAssetFolder {
3
+ id: number;
4
+ name: string;
5
+ parent_id: number | null;
6
+ uuid?: string;
7
+ parent_uuid?: string | null;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface SBAllAssetFoldersRequestResult {
11
+ asset_folders: SBAssetFolder[];
12
+ }
13
+ export type CreateAssetFolderPayload = {
14
+ name: string;
15
+ parent_id?: number | null;
16
+ };
17
+ export type GetAllAssetFolders = ({ spaceId, search, withParent, byIds, byUuids, }: {
18
+ spaceId: string;
19
+ search?: string;
20
+ withParent?: number | string;
21
+ byIds?: Array<number | string>;
22
+ byUuids?: string[];
23
+ }, config: RequestBaseConfig) => Promise<SBAllAssetFoldersRequestResult>;
24
+ export type CreateAssetFolder = ({ spaceId, payload, }: {
25
+ spaceId: string;
26
+ payload: CreateAssetFolderPayload;
27
+ }, config: RequestBaseConfig) => Promise<{
28
+ asset_folder: SBAssetFolder;
29
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,11 @@
1
- import type { CreateAsset, GetAllAssets, GetAssetById, GetAssetByName, MigrateAsset, UpdateAsset } from "./assets.types.js";
1
+ import type { CreateAsset, CreateAssetAndFinalize, GetAllAssets, GetAssetById, GetAssetByName, MigrateAsset, UpdateAsset, DownloadAsset, FinishAssetUpload } from "./assets.types.js";
2
2
  export declare const getAllAssets: GetAllAssets;
3
3
  export declare const getAssetByName: GetAssetByName;
4
+ export declare const finishAssetUpload: FinishAssetUpload;
5
+ export declare const downloadAsset: DownloadAsset;
4
6
  export declare const migrateAsset: MigrateAsset;
5
7
  export declare const createAsset: CreateAsset;
8
+ export declare const createAssetAndFinalize: CreateAssetAndFinalize;
6
9
  export declare const updateAsset: UpdateAsset;
7
10
  export declare const getAssetById: GetAssetById;
8
11
  export declare const getAsset: (assetName: string | undefined) => Promise<void>;
@@ -5,6 +5,7 @@ import FormData from "form-data";
5
5
  import { createDir, isDirectoryExists } from "../../utils/files.js";
6
6
  import Logger from "../../utils/logger.js";
7
7
  import { getFileName, getSizeFromURL } from "../../utils/string-utils.js";
8
+ import { getAllItemsWithPagination } from "../utils/request.js";
8
9
  const isStoryblokSize = (size) => Boolean(size && /^\d+x\d+$/i.test(size));
9
10
  const prepareSignedUploadPayload = (payload) => {
10
11
  const { filename, asset_folder_id, id, size, validate_upload } = payload;
@@ -27,23 +28,26 @@ const prepareSignedUploadPayload = (payload) => {
27
28
  export const getAllAssets = async (args, config) => {
28
29
  const { spaceId, search } = args;
29
30
  const { sbApi } = config;
30
- return sbApi
31
- .get(`spaces/${spaceId}/assets/`, {
32
- // @ts-ignore TODO: have to submit ISSUE to storyblok-js-client (in documentation it is search, in typescript its search_term STORYBLOK_ISSUE
33
- search: search ? search : "",
34
- per_page: 100, // need to do pagination here probably
35
- })
36
- .then(({ data }) => data)
37
- .catch((err) => {
38
- if (err.response.status === 404) {
39
- Logger.error(`There is no assets in your Storyblok ${spaceId} space.`);
40
- return true;
41
- }
42
- else {
31
+ const assets = await getAllItemsWithPagination({
32
+ apiFn: ({ per_page, page }) => sbApi
33
+ .get(`spaces/${spaceId}/assets/`, {
34
+ // @ts-ignore TODO: have to submit ISSUE to storyblok-js-client (in documentation it is search, in typescript its search_term STORYBLOK_ISSUE
35
+ search: search ? search : "",
36
+ per_page,
37
+ page,
38
+ })
39
+ .catch((err) => {
40
+ if (err.response?.status === 404) {
41
+ Logger.error(`There is no assets in your Storyblok ${spaceId} space.`);
42
+ return { data: { assets: [] }, total: 0, perPage: 100 };
43
+ }
43
44
  Logger.error(err);
44
- return false;
45
- }
45
+ throw err;
46
+ }),
47
+ params: {},
48
+ itemsKey: "assets",
46
49
  });
50
+ return { assets };
47
51
  };
48
52
  export const getAssetByName = async ({ spaceId, fileName }, config) => {
49
53
  const result = await getAllAssets({ spaceId, search: fileName }, config);
@@ -95,7 +99,31 @@ const uploadFile = ({ signedResponseObject, pathToFile }) => {
95
99
  });
96
100
  });
97
101
  };
98
- const downloadAsset = async (args, config) => {
102
+ const getSignedUploadAssetId = (signedResponseObject) => {
103
+ const assetId = Number(signedResponseObject?.id ?? signedResponseObject?.asset?.id);
104
+ if (!Number.isFinite(assetId)) {
105
+ throw new Error("Signed upload response did not include an asset id, so upload cannot be finalized.");
106
+ }
107
+ return assetId;
108
+ };
109
+ const normalizeFinishedUploadAsset = (finishedUpload) => {
110
+ const asset = finishedUpload?.asset ?? finishedUpload?.data?.asset ?? finishedUpload;
111
+ if (!asset?.id || !asset?.filename) {
112
+ throw new Error("Finish upload response did not include a target asset id and filename.");
113
+ }
114
+ return asset;
115
+ };
116
+ export const finishAssetUpload = async ({ spaceId, assetId }, config) => {
117
+ const { sbApi } = config;
118
+ return sbApi
119
+ .get(`spaces/${spaceId}/assets/${assetId}/finish_upload`, {})
120
+ .then(({ data }) => data)
121
+ .catch((err) => {
122
+ Logger.error(err);
123
+ throw err;
124
+ });
125
+ };
126
+ export const downloadAsset = async (args, config) => {
99
127
  const { debug, sbmigWorkingDirectory } = config;
100
128
  const { payload } = args;
101
129
  if (!sbmigWorkingDirectory) {
@@ -149,6 +177,23 @@ export const createAsset = async ({ spaceId, pathToFile, payload = {} }, config)
149
177
  await uploadFile({ signedResponseObject, pathToFile });
150
178
  return signedResponseObject;
151
179
  };
180
+ export const createAssetAndFinalize = async ({ spaceId, pathToFile, payload = {} }, config) => {
181
+ const signedResponseObject = await createAsset({
182
+ spaceId,
183
+ pathToFile,
184
+ payload: {
185
+ ...payload,
186
+ filename: payload.filename ?? pathToFile,
187
+ validate_upload: payload.validate_upload ?? 1,
188
+ },
189
+ }, config);
190
+ const assetId = getSignedUploadAssetId(signedResponseObject);
191
+ const finishedUpload = await finishAssetUpload({
192
+ spaceId,
193
+ assetId,
194
+ }, config);
195
+ return normalizeFinishedUploadAsset(finishedUpload);
196
+ };
152
197
  export const updateAsset = async ({ spaceId, assetId, payload }, config) => {
153
198
  const { sbApi } = config;
154
199
  Logger.log(`Trying to update asset with id ${assetId}.`);
@@ -7,7 +7,7 @@ export interface SBAsset {
7
7
  space_id: number;
8
8
  created_at: string;
9
9
  updated_at: string;
10
- asset_folder_id?: null;
10
+ asset_folder_id?: number | null;
11
11
  deleted_at: null;
12
12
  content_length: number;
13
13
  content_type: AssetTypes;
@@ -80,6 +80,11 @@ export type CreateAsset = ({ spaceId, pathToFile, payload, }: {
80
80
  pathToFile: string;
81
81
  payload?: CreateAssetPayload;
82
82
  }, config: RequestBaseConfig) => Promise<SignedResponseObject>;
83
+ export type CreateAssetAndFinalize = ({ spaceId, pathToFile, payload, }: {
84
+ spaceId: string;
85
+ pathToFile: string;
86
+ payload?: CreateAssetPayload;
87
+ }, config: RequestBaseConfig) => Promise<SBAsset>;
83
88
  export type UpdateAsset = ({ spaceId, assetId, payload, }: {
84
89
  spaceId: string;
85
90
  assetId: number;
@@ -92,6 +97,12 @@ export type UploadFile = ({ signedResponseObject, pathToFile, }: {
92
97
  export type FinalizeUpload = ({ signedResponseObject, }: {
93
98
  signedResponseObject: SignedResponseObject;
94
99
  }) => void;
100
+ export type FinishAssetUpload = ({ spaceId, assetId, }: {
101
+ spaceId: string;
102
+ assetId: number;
103
+ }, config: RequestBaseConfig) => Promise<{
104
+ asset: Partial<SBAsset> & Pick<SBAsset, "id" | "filename">;
105
+ }>;
95
106
  export type RequestSignedUploadUrl = ({ spaceId, payload, }: {
96
107
  spaceId: string;
97
108
  payload: AssetPayload | SignedUploadPayload;
@@ -1,2 +1,4 @@
1
- export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, updateAsset, } from "./assets.js";
2
- export type { CreateAssetPayload, SignedUploadPayload, UpdateAssetPayload, } from "./assets.types.js";
1
+ export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, createAssetAndFinalize, downloadAsset, finishAssetUpload, updateAsset, } from "./assets.js";
2
+ export { createAssetFolder, getAllAssetFolders } from "./asset-folders.js";
3
+ export type { CreateAssetAndFinalize, CreateAssetPayload, FinishAssetUpload, SignedUploadPayload, UpdateAssetPayload, } from "./assets.types.js";
4
+ export type { CreateAssetFolder, CreateAssetFolderPayload, SBAllAssetFoldersRequestResult, SBAssetFolder, } from "./asset-folders.types.js";
@@ -1 +1,2 @@
1
- export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, updateAsset, } from "./assets.js";
1
+ export { getAllAssets, createAsset, migrateAsset, getAsset, getAssetById, getAssetByName, createAssetAndFinalize, downloadAsset, finishAssetUpload, updateAsset, } from "./assets.js";
2
+ export { createAssetFolder, getAllAssetFolders } from "./asset-folders.js";
@@ -0,0 +1,12 @@
1
+ import type { CopyGraph } from "./types.js";
2
+ import type { SBAssetFolder } from "../assets/asset-folders.types.js";
3
+ import type { SBAsset } from "../assets/assets.types.js";
4
+ export declare const COPY_ASSETS_DRY_RUN_LIMITATIONS: string[];
5
+ export declare const normalizeAssetFolderParentId: (parentId: number | null | undefined) => number | null;
6
+ export declare const buildCopyAssetsGraph: ({ sourceSpaceId, targetSpaceId, assets, assetFolders, generatedAt, }: {
7
+ sourceSpaceId: string;
8
+ targetSpaceId: string;
9
+ assets: SBAsset[];
10
+ assetFolders: SBAssetFolder[];
11
+ generatedAt?: string;
12
+ }) => CopyGraph;
@@ -0,0 +1,84 @@
1
+ import { createCopyGraph } from "./graph.js";
2
+ export const COPY_ASSETS_DRY_RUN_LIMITATIONS = [
3
+ "target_conflicts_not_checked",
4
+ "target_asset_identity_not_resolved",
5
+ "manifests_not_written_in_dry_run",
6
+ ];
7
+ export const normalizeAssetFolderParentId = (parentId) => (parentId === 0 || parentId == null ? null : parentId);
8
+ const getAssetFolderPath = (folder, folderById) => {
9
+ const names = [folder.name];
10
+ const visited = new Set([folder.id]);
11
+ let parentId = normalizeAssetFolderParentId(folder.parent_id);
12
+ while (parentId !== null) {
13
+ const parent = folderById.get(parentId);
14
+ if (!parent || visited.has(parent.id)) {
15
+ break;
16
+ }
17
+ names.unshift(parent.name);
18
+ visited.add(parent.id);
19
+ parentId = normalizeAssetFolderParentId(parent.parent_id);
20
+ }
21
+ return names.join("/");
22
+ };
23
+ const getAssetFolderDepth = (folder, folderById) => {
24
+ let depth = 0;
25
+ const visited = new Set([folder.id]);
26
+ let parentId = normalizeAssetFolderParentId(folder.parent_id);
27
+ while (parentId !== null) {
28
+ const parent = folderById.get(parentId);
29
+ if (!parent || visited.has(parent.id)) {
30
+ break;
31
+ }
32
+ depth += 1;
33
+ visited.add(parent.id);
34
+ parentId = normalizeAssetFolderParentId(parent.parent_id);
35
+ }
36
+ return depth;
37
+ };
38
+ export const buildCopyAssetsGraph = ({ sourceSpaceId, targetSpaceId, assets, assetFolders, generatedAt, }) => {
39
+ const graph = createCopyGraph({
40
+ sourceSpaceId,
41
+ targetSpaceId,
42
+ generatedAt,
43
+ scope: {
44
+ command: "copy assets",
45
+ },
46
+ });
47
+ graph.limitations.push(...COPY_ASSETS_DRY_RUN_LIMITATIONS);
48
+ const folderById = new Map(assetFolders.map((folder) => [folder.id, folder]));
49
+ const folderNodes = assetFolders
50
+ .map((folder) => ({
51
+ type: "asset_folder",
52
+ sourceId: folder.id,
53
+ sourcePath: getAssetFolderPath(folder, folderById),
54
+ targetPath: getAssetFolderPath(folder, folderById),
55
+ sourceParentId: normalizeAssetFolderParentId(folder.parent_id),
56
+ action: "create",
57
+ depth: getAssetFolderDepth(folder, folderById),
58
+ }))
59
+ .sort((left, right) => left.depth - right.depth || left.sourceId - right.sourceId)
60
+ .map(({ depth: _depth, ...node }) => node);
61
+ graph.assetFolders.push(...folderNodes);
62
+ for (const folder of assetFolders) {
63
+ const parentId = normalizeAssetFolderParentId(folder.parent_id);
64
+ if (parentId !== null && !folderById.has(parentId)) {
65
+ graph.warnings.push({
66
+ code: "asset_folder_parent_missing",
67
+ message: `Asset folder '${folder.name}' references parent id ${parentId}, but that parent was not returned by Storyblok.`,
68
+ path: getAssetFolderPath(folder, folderById),
69
+ sourceValue: parentId,
70
+ });
71
+ }
72
+ }
73
+ graph.assets.push(...assets
74
+ .map((asset) => ({
75
+ type: "asset",
76
+ sourceId: asset.id,
77
+ sourceFilename: asset.filename,
78
+ targetFilename: asset.filename,
79
+ sourceAssetFolderId: asset.asset_folder_id ?? null,
80
+ action: "create",
81
+ }))
82
+ .sort((left, right) => left.sourceId - right.sourceId));
83
+ return graph;
84
+ };
@@ -0,0 +1,8 @@
1
+ import type { CopyGraph, CopyGraphSummary, CopyScope } from "./types.js";
2
+ export declare const createCopyGraph: ({ sourceSpaceId, targetSpaceId, scope, generatedAt, }: {
3
+ sourceSpaceId: string;
4
+ targetSpaceId: string;
5
+ scope: CopyScope;
6
+ generatedAt?: string;
7
+ }) => CopyGraph;
8
+ export declare const summarizeCopyGraph: (graph: CopyGraph) => CopyGraphSummary;
@@ -0,0 +1,27 @@
1
+ export const createCopyGraph = ({ sourceSpaceId, targetSpaceId, scope, generatedAt = new Date().toISOString(), }) => ({
2
+ schemaVersion: 1,
3
+ sourceSpaceId,
4
+ targetSpaceId,
5
+ generatedAt,
6
+ scope,
7
+ stories: [],
8
+ assets: [],
9
+ assetFolders: [],
10
+ storyReferences: [],
11
+ assetReferences: [],
12
+ opaqueFields: [],
13
+ warnings: [],
14
+ errors: [],
15
+ limitations: [],
16
+ });
17
+ export const summarizeCopyGraph = (graph) => ({
18
+ stories: graph.stories.length,
19
+ assets: graph.assets.length,
20
+ assetFolders: graph.assetFolders.length,
21
+ storyReferences: graph.storyReferences.length,
22
+ assetReferences: graph.assetReferences.length,
23
+ opaqueFields: graph.opaqueFields.length,
24
+ warnings: graph.warnings.length,
25
+ errors: graph.errors.length,
26
+ limitations: graph.limitations.length,
27
+ });
@@ -0,0 +1,6 @@
1
+ export * from "./graph.js";
2
+ export * from "./manifest.js";
3
+ export * from "./reference-scanner.js";
4
+ export * from "./reference-rewriter.js";
5
+ export * from "./assets.js";
6
+ export * from "./types.js";
@@ -0,0 +1,6 @@
1
+ export * from "./graph.js";
2
+ export * from "./manifest.js";
3
+ export * from "./reference-scanner.js";
4
+ export * from "./reference-rewriter.js";
5
+ export * from "./assets.js";
6
+ export * from "./types.js";
@@ -0,0 +1,23 @@
1
+ import type { CopyManifestEntry, CopyMaps } from "./types.js";
2
+ export type CopyManifestPaths = {
3
+ rootDir: string;
4
+ combined: string;
5
+ stories: string;
6
+ assets: string;
7
+ assetFolders: string;
8
+ report: string;
9
+ };
10
+ export declare const getDefaultCopyManifestPaths: ({ sourceSpaceId, targetSpaceId, rootDir, }: {
11
+ sourceSpaceId: string;
12
+ targetSpaceId: string;
13
+ rootDir?: string;
14
+ }) => CopyManifestPaths;
15
+ export declare const createEmptyCopyMaps: () => CopyMaps;
16
+ export declare const loadManifest: <T extends CopyManifestEntry>(filePath: string) => Promise<T[]>;
17
+ export declare const parseManifestJsonl: <T extends CopyManifestEntry>(content: string, filePath?: string) => T[];
18
+ export declare const appendManifestEntry: (filePath: string, entry: CopyManifestEntry) => Promise<void>;
19
+ export declare const appendManifestEntries: (filePath: string, entries: CopyManifestEntry[]) => Promise<void>;
20
+ export declare const writeManifest: (filePath: string, entries: CopyManifestEntry[]) => Promise<void>;
21
+ export declare const dedupeManifestEntries: <T extends CopyManifestEntry>(entries: T[]) => T[];
22
+ export declare const dedupeManifestFile: (filePath: string) => Promise<CopyManifestEntry[]>;
23
+ export declare const buildCopyMaps: (entries: CopyManifestEntry[]) => CopyMaps;
@@ -0,0 +1,115 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ export const getDefaultCopyManifestPaths = ({ sourceSpaceId, targetSpaceId, rootDir = ".sb-mig", }) => {
4
+ const copyRoot = path.join(rootDir, "copy", sourceSpaceId, targetSpaceId);
5
+ return {
6
+ rootDir: copyRoot,
7
+ combined: path.join(copyRoot, "manifest.jsonl"),
8
+ stories: path.join(copyRoot, "stories.manifest.jsonl"),
9
+ assets: path.join(copyRoot, "assets.manifest.jsonl"),
10
+ assetFolders: path.join(copyRoot, "asset-folders.manifest.jsonl"),
11
+ report: path.join(copyRoot, "report.json"),
12
+ };
13
+ };
14
+ export const createEmptyCopyMaps = () => ({
15
+ storyIds: new Map(),
16
+ storyUuids: new Map(),
17
+ assetIds: new Map(),
18
+ assetFilenames: new Map(),
19
+ assetFolderIds: new Map(),
20
+ });
21
+ export const loadManifest = async (filePath) => {
22
+ try {
23
+ const content = await fs.readFile(filePath, "utf8");
24
+ return parseManifestJsonl(content, filePath);
25
+ }
26
+ catch (error) {
27
+ if (error?.code === "ENOENT") {
28
+ return [];
29
+ }
30
+ throw error;
31
+ }
32
+ };
33
+ export const parseManifestJsonl = (content, filePath = "manifest.jsonl") => content
34
+ .split("\n")
35
+ .map((line, index) => ({ line: line.trim(), lineNumber: index + 1 }))
36
+ .filter(({ line }) => line.length > 0)
37
+ .map(({ line, lineNumber }) => {
38
+ try {
39
+ return JSON.parse(line);
40
+ }
41
+ catch (error) {
42
+ throw new Error(`Failed to parse manifest '${filePath}' at line ${lineNumber}: ${error.message}`);
43
+ }
44
+ });
45
+ export const appendManifestEntry = async (filePath, entry) => {
46
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
47
+ await fs.appendFile(filePath, `${JSON.stringify(entry)}\n`, "utf8");
48
+ };
49
+ export const appendManifestEntries = async (filePath, entries) => {
50
+ if (entries.length === 0) {
51
+ return;
52
+ }
53
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
54
+ await fs.appendFile(filePath, entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n", "utf8");
55
+ };
56
+ export const writeManifest = async (filePath, entries) => {
57
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
58
+ const content = entries.length > 0
59
+ ? entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n"
60
+ : "";
61
+ await fs.writeFile(filePath, content, "utf8");
62
+ };
63
+ export const dedupeManifestEntries = (entries) => {
64
+ const byKey = new Map();
65
+ for (const entry of entries) {
66
+ byKey.set(getManifestEntrySourceKey(entry), entry);
67
+ }
68
+ return Array.from(byKey.values());
69
+ };
70
+ export const dedupeManifestFile = async (filePath) => {
71
+ const entries = await loadManifest(filePath);
72
+ const deduped = dedupeManifestEntries(entries);
73
+ await writeManifest(filePath, deduped);
74
+ return deduped;
75
+ };
76
+ export const buildCopyMaps = (entries) => {
77
+ const maps = createEmptyCopyMaps();
78
+ for (const entry of entries) {
79
+ if (isStoryManifestEntry(entry)) {
80
+ maps.storyIds.set(entry.source_id, entry.target_id);
81
+ maps.storyUuids.set(entry.source_uuid, entry.target_uuid);
82
+ }
83
+ if (isAssetManifestEntry(entry)) {
84
+ maps.assetIds.set(entry.source_id, {
85
+ id: entry.target_id,
86
+ filename: entry.target_filename,
87
+ });
88
+ maps.assetFilenames.set(entry.source_filename, entry.target_filename);
89
+ }
90
+ if (isAssetFolderManifestEntry(entry)) {
91
+ maps.assetFolderIds.set(entry.source_id, entry.target_id);
92
+ }
93
+ }
94
+ return maps;
95
+ };
96
+ const getManifestEntrySourceKey = (entry) => {
97
+ if (isStoryManifestEntry(entry)) {
98
+ return [
99
+ entry.type,
100
+ entry.source_space_id,
101
+ entry.target_space_id,
102
+ entry.source_id,
103
+ entry.source_uuid,
104
+ ].join(":");
105
+ }
106
+ return [
107
+ entry.type,
108
+ entry.source_space_id,
109
+ entry.target_space_id,
110
+ entry.source_id,
111
+ ].join(":");
112
+ };
113
+ const isStoryManifestEntry = (entry) => entry.type === "story";
114
+ const isAssetManifestEntry = (entry) => entry.type === "asset";
115
+ const isAssetFolderManifestEntry = (entry) => entry.type === "asset_folder";
@@ -0,0 +1,6 @@
1
+ import type { CopyComponentSchemaRegistry, CopyMaps, CopyRewriteResult } from "./types.js";
2
+ export declare const rewriteCopyReferences: <T>({ value, maps, schemas, }: {
3
+ value: T;
4
+ maps: CopyMaps;
5
+ schemas?: CopyComponentSchemaRegistry;
6
+ }) => CopyRewriteResult<T>;