@rglabs/butterfly 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/CLAUDE.md +201 -0
  2. package/README.md +371 -0
  3. package/dist/commands/add.d.ts +23 -0
  4. package/dist/commands/add.js +303 -0
  5. package/dist/commands/code.d.ts +11 -0
  6. package/dist/commands/code.js +72 -0
  7. package/dist/commands/create-object.d.ts +6 -0
  8. package/dist/commands/create-object.js +293 -0
  9. package/dist/commands/create-report.d.ts +6 -0
  10. package/dist/commands/create-report.js +154 -0
  11. package/dist/commands/diff.d.ts +4 -0
  12. package/dist/commands/diff.js +238 -0
  13. package/dist/commands/download.d.ts +4 -0
  14. package/dist/commands/download.js +374 -0
  15. package/dist/commands/layout.d.ts +12 -0
  16. package/dist/commands/layout.js +83 -0
  17. package/dist/commands/record.d.ts +21 -0
  18. package/dist/commands/record.js +483 -0
  19. package/dist/commands/run-poc.d.ts +3 -0
  20. package/dist/commands/run-poc.js +18 -0
  21. package/dist/commands/setup.d.ts +3 -0
  22. package/dist/commands/setup.js +66 -0
  23. package/dist/commands/start-poc.d.ts +3 -0
  24. package/dist/commands/start-poc.js +55 -0
  25. package/dist/commands/sync-docs.d.ts +3 -0
  26. package/dist/commands/sync-docs.js +27 -0
  27. package/dist/commands/translate.d.ts +13 -0
  28. package/dist/commands/translate.js +401 -0
  29. package/dist/commands/upload.d.ts +3 -0
  30. package/dist/commands/upload.js +150 -0
  31. package/dist/commands/workflow-info.d.ts +13 -0
  32. package/dist/commands/workflow-info.js +161 -0
  33. package/dist/components/ConflictResolver.d.ts +12 -0
  34. package/dist/components/ConflictResolver.js +77 -0
  35. package/dist/components/DiffView.d.ts +11 -0
  36. package/dist/components/DiffView.js +101 -0
  37. package/dist/components/DownloadProgress.d.ts +11 -0
  38. package/dist/components/DownloadProgress.js +29 -0
  39. package/dist/components/RecordPreview.d.ts +11 -0
  40. package/dist/components/RecordPreview.js +91 -0
  41. package/dist/components/SetupForm.d.ts +8 -0
  42. package/dist/components/SetupForm.js +56 -0
  43. package/dist/components/UploadProgress.d.ts +13 -0
  44. package/dist/components/UploadProgress.js +42 -0
  45. package/dist/diff/adapters/index.d.ts +8 -0
  46. package/dist/diff/adapters/index.js +18 -0
  47. package/dist/diff/adapters/objectsAdapter.d.ts +13 -0
  48. package/dist/diff/adapters/objectsAdapter.js +177 -0
  49. package/dist/diff/adapters/reportsAdapter.d.ts +14 -0
  50. package/dist/diff/adapters/reportsAdapter.js +212 -0
  51. package/dist/diff/adapters/types.d.ts +19 -0
  52. package/dist/diff/adapters/types.js +2 -0
  53. package/dist/diff/engine.d.ts +19 -0
  54. package/dist/diff/engine.js +57 -0
  55. package/dist/diff/types.d.ts +34 -0
  56. package/dist/diff/types.js +110 -0
  57. package/dist/index.d.ts +3 -0
  58. package/dist/index.js +117 -0
  59. package/dist/types/index.d.ts +18 -0
  60. package/dist/types/index.js +2 -0
  61. package/dist/utils/api.d.ts +85 -0
  62. package/dist/utils/api.js +1031 -0
  63. package/dist/utils/auth.d.ts +4 -0
  64. package/dist/utils/auth.js +22 -0
  65. package/dist/utils/bfySplitter.d.ts +12 -0
  66. package/dist/utils/bfySplitter.js +151 -0
  67. package/dist/utils/docs.d.ts +16 -0
  68. package/dist/utils/docs.js +186 -0
  69. package/dist/utils/errorLogger.d.ts +6 -0
  70. package/dist/utils/errorLogger.js +29 -0
  71. package/dist/utils/files.d.ts +14 -0
  72. package/dist/utils/files.js +772 -0
  73. package/dist/utils/lockManager.d.ts +15 -0
  74. package/dist/utils/lockManager.js +126 -0
  75. package/dist/utils/resourceHandlers.d.ts +50 -0
  76. package/dist/utils/resourceHandlers.js +684 -0
  77. package/dist/utils/resourceMapping.d.ts +32 -0
  78. package/dist/utils/resourceMapping.js +210 -0
  79. package/dist/utils/singleResourceDownload.d.ts +14 -0
  80. package/dist/utils/singleResourceDownload.js +261 -0
  81. package/dist/utils/summaryGenerator.d.ts +2 -0
  82. package/dist/utils/summaryGenerator.js +183 -0
  83. package/dist/utils/uploadHandler.d.ts +31 -0
  84. package/dist/utils/uploadHandler.js +263 -0
  85. package/docs/AI_API.md +93 -0
  86. package/docs/CLAUDE.md +216 -0
  87. package/docs/PROJECT_SPECIFIC.md +1 -0
  88. package/docs/RECORD_COMMAND.md +262 -0
  89. package/docs/WORKFLOW_API.md +480 -0
  90. package/docs/bfy-splitting.md +126 -0
  91. package/docs/cli-commands.md +333 -0
  92. package/docs/examples/README.md +95 -0
  93. package/docs/examples/order-system.md +147 -0
  94. package/docs/examples/product-catalog.md +195 -0
  95. package/docs/examples/reports.md +187 -0
  96. package/docs/excel-export.md +216 -0
  97. package/docs/field-types/README.md +29 -0
  98. package/docs/field-types/calculated.md +147 -0
  99. package/docs/field-types/code-mappings.md +84 -0
  100. package/docs/field-types/custom.md +340 -0
  101. package/docs/object-specs/README.md +136 -0
  102. package/docs/object-specs/code-parameters.md +151 -0
  103. package/docs/object-specs/creating.md +203 -0
  104. package/docs/object-specs/js-code-examples.md +208 -0
  105. package/docs/object-specs/js-field-updates.md +168 -0
  106. package/docs/objects/README.md +89 -0
  107. package/docs/objects/creating.md +127 -0
  108. package/docs/page-layout.md +361 -0
  109. package/docs/permissions.md +260 -0
  110. package/docs/reports.md +197 -0
  111. package/docs/state-machines.md +544 -0
  112. package/docs/tasks/create-object.md +81 -0
  113. package/docs/translations.md +346 -0
  114. package/docs/twig-helpers.md +283 -0
  115. package/docs/webservices.md +159 -0
  116. package/docs/workspaces.md +176 -0
  117. package/package.json +59 -0
@@ -0,0 +1,91 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ function formatValue(value, maxLength = 100) {
4
+ if (value === null)
5
+ return '<null>';
6
+ if (value === undefined)
7
+ return '<undefined>';
8
+ if (typeof value === 'object') {
9
+ const json = JSON.stringify(value);
10
+ if (json.length > maxLength) {
11
+ return json.substring(0, maxLength) + `... (${json.length} chars)`;
12
+ }
13
+ return json;
14
+ }
15
+ const str = String(value);
16
+ if (str.length > maxLength) {
17
+ return str.substring(0, maxLength) + `... (${str.length} chars)`;
18
+ }
19
+ return str;
20
+ }
21
+ function getChanges(existingRecord, newData) {
22
+ const changes = [];
23
+ for (const [key, newValue] of Object.entries(newData)) {
24
+ if (key === 'id')
25
+ continue;
26
+ const oldValue = existingRecord[key];
27
+ if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) {
28
+ changes.push({ field: key, oldValue, newValue });
29
+ }
30
+ }
31
+ return changes;
32
+ }
33
+ export const RecordPreview = ({ operation, table, data, existingRecord }) => {
34
+ const operationColors = {
35
+ get: 'cyan',
36
+ add: 'green',
37
+ edit: 'yellow',
38
+ delete: 'red'
39
+ };
40
+ const changes = operation === 'edit' && existingRecord
41
+ ? getChanges(existingRecord, data)
42
+ : null;
43
+ return (React.createElement(Box, { flexDirection: "column" },
44
+ React.createElement(Box, { marginBottom: 1 },
45
+ React.createElement(Text, { bold: true }, "Operation: "),
46
+ React.createElement(Text, { color: operationColors[operation], bold: true }, operation.toUpperCase())),
47
+ React.createElement(Box, { marginBottom: 1 },
48
+ React.createElement(Text, { bold: true }, "Table: "),
49
+ React.createElement(Text, { color: "cyan" }, table)),
50
+ data.id && (React.createElement(Box, { marginBottom: 1 },
51
+ React.createElement(Text, { bold: true }, "Record ID: "),
52
+ React.createElement(Text, null, data.id))),
53
+ operation === 'add' && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
54
+ React.createElement(Text, { bold: true, underline: true }, "New Record Data:"),
55
+ Object.entries(data).map(([key, value]) => (React.createElement(Box, { key: key, marginLeft: 2 },
56
+ React.createElement(Text, { color: "green" },
57
+ "+ ",
58
+ key,
59
+ ": "),
60
+ React.createElement(Text, null, formatValue(value))))))),
61
+ operation === 'edit' && changes && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
62
+ React.createElement(Text, { bold: true, underline: true }, "Changes:"),
63
+ changes.length === 0 ? (React.createElement(Box, { marginLeft: 2 },
64
+ React.createElement(Text, { color: "gray" }, "No changes detected"))) : (changes.map(({ field, oldValue, newValue }) => (React.createElement(Box, { key: field, flexDirection: "column", marginLeft: 2 },
65
+ React.createElement(Text, { bold: true },
66
+ field,
67
+ ":"),
68
+ React.createElement(Box, { marginLeft: 2 },
69
+ React.createElement(Text, { color: "red" },
70
+ "- ",
71
+ formatValue(oldValue))),
72
+ React.createElement(Box, { marginLeft: 2 },
73
+ React.createElement(Text, { color: "green" },
74
+ "+ ",
75
+ formatValue(newValue))))))))),
76
+ operation === 'delete' && existingRecord && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
77
+ React.createElement(Text, { bold: true, underline: true, color: "red" }, "Record to Delete:"),
78
+ Object.entries(existingRecord).slice(0, 15).map(([key, value]) => (React.createElement(Box, { key: key, marginLeft: 2 },
79
+ React.createElement(Text, { color: "red" },
80
+ "- ",
81
+ key,
82
+ ": "),
83
+ React.createElement(Text, null, formatValue(value))))),
84
+ Object.keys(existingRecord).length > 15 && (React.createElement(Box, { marginLeft: 2 },
85
+ React.createElement(Text, { color: "gray" },
86
+ "... and ",
87
+ Object.keys(existingRecord).length - 15,
88
+ " more fields")))))));
89
+ };
90
+ export default RecordPreview;
91
+ //# sourceMappingURL=RecordPreview.js.map
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { AuthConfig } from '../types/index.js';
3
+ interface SetupFormProps {
4
+ onSubmit: (config: AuthConfig) => void;
5
+ }
6
+ declare const SetupForm: React.FC<SetupFormProps>;
7
+ export { SetupForm };
8
+ //# sourceMappingURL=SetupForm.d.ts.map
@@ -0,0 +1,56 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import TextInput from 'ink-text-input';
4
+ const SetupForm = ({ onSubmit }) => {
5
+ const [step, setStep] = useState(0);
6
+ const [endpoint, setEndpoint] = useState('');
7
+ const [email, setEmail] = useState('');
8
+ const [password, setPassword] = useState('');
9
+ const [core, setCore] = useState(false);
10
+ const handleSubmit = () => {
11
+ if (step === 0 && endpoint) {
12
+ setStep(1);
13
+ }
14
+ else if (step === 1 && email) {
15
+ setStep(2);
16
+ }
17
+ else if (step === 2 && password) {
18
+ setStep(3);
19
+ }
20
+ else if (step === 3) {
21
+ onSubmit({ endpoint, email, password, core });
22
+ }
23
+ };
24
+ const handleCoreToggle = () => {
25
+ setCore(!core);
26
+ };
27
+ useInput((input, key) => {
28
+ if (step === 3) {
29
+ if (input === ' ') {
30
+ handleCoreToggle();
31
+ }
32
+ else if (key.return) {
33
+ handleSubmit();
34
+ }
35
+ }
36
+ });
37
+ return (React.createElement(Box, { flexDirection: "column" },
38
+ React.createElement(Text, { color: "green" }, "Butterfly CLI Setup"),
39
+ React.createElement(Text, null, " "),
40
+ step >= 0 && (React.createElement(Box, null,
41
+ React.createElement(Text, null, "Endpoint: "),
42
+ step === 0 ? (React.createElement(TextInput, { value: endpoint, onChange: setEndpoint, onSubmit: handleSubmit })) : (React.createElement(Text, { color: "cyan" }, endpoint)))),
43
+ step >= 1 && (React.createElement(Box, null,
44
+ React.createElement(Text, null, "Email: "),
45
+ step === 1 ? (React.createElement(TextInput, { value: email, onChange: setEmail, onSubmit: handleSubmit })) : (React.createElement(Text, { color: "cyan" }, email)))),
46
+ step >= 2 && (React.createElement(Box, null,
47
+ React.createElement(Text, null, "Password: "),
48
+ step === 2 ? (React.createElement(TextInput, { value: password, onChange: setPassword, onSubmit: handleSubmit, mask: "*" })) : (React.createElement(Text, { color: "cyan" }, '*'.repeat(password.length))))),
49
+ step >= 3 && (React.createElement(Box, null,
50
+ React.createElement(Text, null, "Enable Core Features: "),
51
+ React.createElement(Text, { color: core ? "green" : "gray" },
52
+ core ? "[✓] Yes" : "[ ] No",
53
+ React.createElement(Text, { color: "gray" }, " (Press space to toggle, enter to continue)"))))));
54
+ };
55
+ export { SetupForm };
56
+ //# sourceMappingURL=SetupForm.js.map
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ interface UploadProgressProps {
3
+ current: number;
4
+ total: number;
5
+ currentFile: string;
6
+ status: 'uploading' | 'complete' | 'error';
7
+ error?: string;
8
+ succeeded?: number;
9
+ failed?: number;
10
+ }
11
+ declare const UploadProgress: React.FC<UploadProgressProps>;
12
+ export { UploadProgress };
13
+ //# sourceMappingURL=UploadProgress.d.ts.map
@@ -0,0 +1,42 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ const UploadProgress = ({ current, total, currentFile, status, error, succeeded = 0, failed = 0, }) => {
5
+ return (React.createElement(Box, null,
6
+ status === 'uploading' && (React.createElement(React.Fragment, null,
7
+ React.createElement(Text, { color: "cyan" },
8
+ React.createElement(Spinner, { type: "dots" })),
9
+ React.createElement(Text, null,
10
+ " Uploading (",
11
+ current,
12
+ "/",
13
+ total,
14
+ ")",
15
+ currentFile ? `: ${currentFile}` : ''))),
16
+ status === 'complete' && failed === 0 && (React.createElement(Text, { color: "green" },
17
+ "\u2713 Uploaded ",
18
+ succeeded,
19
+ " file",
20
+ succeeded !== 1 ? 's' : '')),
21
+ status === 'complete' && failed > 0 && succeeded > 0 && (React.createElement(Text, null,
22
+ React.createElement(Text, { color: "yellow" },
23
+ "\u26A0 Uploaded ",
24
+ succeeded,
25
+ " file",
26
+ succeeded !== 1 ? 's' : '',
27
+ ", "),
28
+ React.createElement(Text, { color: "red" },
29
+ failed,
30
+ " failed"))),
31
+ status === 'complete' && failed > 0 && succeeded === 0 && (React.createElement(Text, { color: "red" },
32
+ "\u2717 All ",
33
+ failed,
34
+ " upload",
35
+ failed !== 1 ? 's' : '',
36
+ " failed")),
37
+ status === 'error' && (React.createElement(Text, { color: "red" },
38
+ "\u2717 ",
39
+ error))));
40
+ };
41
+ export { UploadProgress };
42
+ //# sourceMappingURL=UploadProgress.js.map
@@ -0,0 +1,8 @@
1
+ export { ResourceAdapter, LocalResource, RemoteResource } from './types.js';
2
+ export { ObjectsAdapter } from './objectsAdapter.js';
3
+ export { ReportsAdapter } from './reportsAdapter.js';
4
+ import { ResourceAdapter } from './types.js';
5
+ export declare function getAdapters(): Map<string, ResourceAdapter>;
6
+ export declare function getAdapter(resourceType: string): ResourceAdapter | undefined;
7
+ export declare function getSupportedTypes(): string[];
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,18 @@
1
+ export { ObjectsAdapter } from './objectsAdapter.js';
2
+ export { ReportsAdapter } from './reportsAdapter.js';
3
+ import { ObjectsAdapter } from './objectsAdapter.js';
4
+ import { ReportsAdapter } from './reportsAdapter.js';
5
+ export function getAdapters() {
6
+ const adapters = new Map();
7
+ adapters.set('objects', new ObjectsAdapter());
8
+ adapters.set('reports', new ReportsAdapter());
9
+ return adapters;
10
+ }
11
+ export function getAdapter(resourceType) {
12
+ const adapters = getAdapters();
13
+ return adapters.get(resourceType);
14
+ }
15
+ export function getSupportedTypes() {
16
+ return Array.from(getAdapters().keys());
17
+ }
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ import { ButterflyAPI } from '../../utils/api.js';
2
+ import { ResourceAdapter, LocalResource, RemoteResource } from './types.js';
3
+ import { ResourceDiff } from '../types.js';
4
+ export declare class ObjectsAdapter implements ResourceAdapter {
5
+ readonly resourceType = "objects";
6
+ loadLocal(basePath: string): Promise<Map<number, LocalResource>>;
7
+ private loadLocalSpecs;
8
+ fetchRemote(api: ButterflyAPI): Promise<Map<number, RemoteResource>>;
9
+ compare(local: LocalResource | null, remote: RemoteResource | null, localPath?: string): ResourceDiff;
10
+ private compareSpecs;
11
+ getDisplayName(data: Record<string, any>): string;
12
+ }
13
+ //# sourceMappingURL=objectsAdapter.d.ts.map
@@ -0,0 +1,177 @@
1
+ import { promises as fs } from 'fs';
2
+ import { join } from 'path';
3
+ import { restoreFieldsFromFiles } from '../../utils/files.js';
4
+ import { compareFields } from '../types.js';
5
+ export class ObjectsAdapter {
6
+ resourceType = 'objects';
7
+ async loadLocal(basePath) {
8
+ const objects = new Map();
9
+ const objectsPath = join(basePath, 'objects');
10
+ let subfolders = [];
11
+ try {
12
+ subfolders = await fs.readdir(objectsPath);
13
+ }
14
+ catch {
15
+ return objects;
16
+ }
17
+ for (const subfolder of subfolders) {
18
+ const subPath = join(objectsPath, subfolder);
19
+ let stat;
20
+ try {
21
+ stat = await fs.stat(subPath);
22
+ }
23
+ catch {
24
+ continue;
25
+ }
26
+ if (!stat.isDirectory())
27
+ continue;
28
+ let tables = [];
29
+ try {
30
+ tables = await fs.readdir(subPath);
31
+ }
32
+ catch {
33
+ continue;
34
+ }
35
+ for (const tableName of tables) {
36
+ const tablePath = join(subPath, tableName);
37
+ const objectJsonPath = join(tablePath, 'object.json');
38
+ try {
39
+ const objectContent = await fs.readFile(objectJsonPath, 'utf-8');
40
+ const objectData = JSON.parse(objectContent);
41
+ const restored = await restoreFieldsFromFiles(tablePath, objectData);
42
+ const specs = await this.loadLocalSpecs(tablePath);
43
+ objects.set(restored.id, {
44
+ main: restored,
45
+ children: { specs },
46
+ path: tablePath
47
+ });
48
+ }
49
+ catch {
50
+ }
51
+ }
52
+ }
53
+ return objects;
54
+ }
55
+ async loadLocalSpecs(tablePath) {
56
+ const specs = [];
57
+ let entries = [];
58
+ try {
59
+ entries = await fs.readdir(tablePath);
60
+ }
61
+ catch {
62
+ return specs;
63
+ }
64
+ for (const entry of entries) {
65
+ if (entry === 'object.json' || entry === 'listing_query.bfy')
66
+ continue;
67
+ const entryPath = join(tablePath, entry);
68
+ let stat;
69
+ try {
70
+ stat = await fs.stat(entryPath);
71
+ }
72
+ catch {
73
+ continue;
74
+ }
75
+ if (!stat.isDirectory())
76
+ continue;
77
+ const specJsonPath = join(entryPath, 'spec.json');
78
+ try {
79
+ const specContent = await fs.readFile(specJsonPath, 'utf-8');
80
+ const specData = JSON.parse(specContent);
81
+ const restored = await restoreFieldsFromFiles(entryPath, specData);
82
+ specs.push(restored);
83
+ }
84
+ catch {
85
+ }
86
+ }
87
+ return specs;
88
+ }
89
+ async fetchRemote(api) {
90
+ const objects = new Map();
91
+ const allObjects = await api.fetchTable('objects');
92
+ const allSpecs = await api.fetchTable('object_specs');
93
+ const specsByObjectId = new Map();
94
+ for (const spec of allSpecs) {
95
+ const list = specsByObjectId.get(spec.object_id) || [];
96
+ list.push(spec);
97
+ specsByObjectId.set(spec.object_id, list);
98
+ }
99
+ for (const obj of allObjects) {
100
+ objects.set(obj.id, {
101
+ main: obj,
102
+ children: {
103
+ specs: specsByObjectId.get(obj.id) || []
104
+ }
105
+ });
106
+ }
107
+ return objects;
108
+ }
109
+ compare(local, remote, localPath) {
110
+ const resourceId = local?.main?.id || remote?.main?.id || 0;
111
+ const resourceName = this.getDisplayName(local?.main || remote?.main || {});
112
+ let status;
113
+ if (!local && remote) {
114
+ status = 'remote_only';
115
+ }
116
+ else if (local && !remote) {
117
+ status = 'local_only';
118
+ }
119
+ else if (local && remote) {
120
+ const fieldDiffs = compareFields(local.main, remote.main);
121
+ status = fieldDiffs.length > 0 ? 'modified' : 'unchanged';
122
+ }
123
+ else {
124
+ status = 'unchanged';
125
+ }
126
+ const fieldDiffs = compareFields(local?.main || null, remote?.main || null);
127
+ const childDiffs = this.compareSpecs(local?.children?.specs || [], remote?.children?.specs || []);
128
+ if (childDiffs.length > 0 && status === 'unchanged') {
129
+ status = 'modified';
130
+ }
131
+ return {
132
+ resourceType: this.resourceType,
133
+ resourceName,
134
+ resourceId,
135
+ localPath: localPath || local?.path,
136
+ status,
137
+ fieldDiffs,
138
+ children: childDiffs
139
+ };
140
+ }
141
+ compareSpecs(localSpecs, remoteSpecs) {
142
+ const diffs = [];
143
+ const localById = new Map(localSpecs.map(s => [s.id, s]));
144
+ const remoteById = new Map(remoteSpecs.map(s => [s.id, s]));
145
+ const allIds = new Set([...localById.keys(), ...remoteById.keys()]);
146
+ for (const specId of allIds) {
147
+ const localSpec = localById.get(specId);
148
+ const remoteSpec = remoteById.get(specId);
149
+ let status;
150
+ if (!localSpec && remoteSpec) {
151
+ status = 'remote_only';
152
+ }
153
+ else if (localSpec && !remoteSpec) {
154
+ status = 'local_only';
155
+ }
156
+ else {
157
+ const fieldDiffs = compareFields(localSpec, remoteSpec);
158
+ status = fieldDiffs.length > 0 ? 'modified' : 'unchanged';
159
+ }
160
+ if (status !== 'unchanged') {
161
+ const specName = localSpec?.column_name || remoteSpec?.column_name || `spec_${specId}`;
162
+ diffs.push({
163
+ resourceType: 'object_specs',
164
+ resourceName: specName,
165
+ resourceId: specId,
166
+ status,
167
+ fieldDiffs: compareFields(localSpec || null, remoteSpec || null)
168
+ });
169
+ }
170
+ }
171
+ return diffs;
172
+ }
173
+ getDisplayName(data) {
174
+ return data.table_name || data.name || `object_${data.id}`;
175
+ }
176
+ }
177
+ //# sourceMappingURL=objectsAdapter.js.map
@@ -0,0 +1,14 @@
1
+ import { ButterflyAPI } from '../../utils/api.js';
2
+ import { ResourceAdapter, LocalResource, RemoteResource } from './types.js';
3
+ import { ResourceDiff } from '../types.js';
4
+ export declare class ReportsAdapter implements ResourceAdapter {
5
+ readonly resourceType = "reports";
6
+ loadLocal(basePath: string): Promise<Map<number, LocalResource>>;
7
+ private loadLocalQueries;
8
+ private loadLocalSpecs;
9
+ fetchRemote(api: ButterflyAPI): Promise<Map<number, RemoteResource>>;
10
+ compare(local: LocalResource | null, remote: RemoteResource | null, localPath?: string): ResourceDiff;
11
+ private compareChildren;
12
+ getDisplayName(data: Record<string, any>): string;
13
+ }
14
+ //# sourceMappingURL=reportsAdapter.d.ts.map
@@ -0,0 +1,212 @@
1
+ import { promises as fs } from 'fs';
2
+ import { join } from 'path';
3
+ import { restoreFieldsFromFiles } from '../../utils/files.js';
4
+ import { compareFields } from '../types.js';
5
+ export class ReportsAdapter {
6
+ resourceType = 'reports';
7
+ async loadLocal(basePath) {
8
+ const reports = new Map();
9
+ const reportsPath = join(basePath, 'reports');
10
+ let reportFolders = [];
11
+ try {
12
+ reportFolders = await fs.readdir(reportsPath);
13
+ }
14
+ catch {
15
+ return reports;
16
+ }
17
+ for (const folder of reportFolders) {
18
+ if (folder === 'categories.json')
19
+ continue;
20
+ const reportPath = join(reportsPath, folder);
21
+ let stat;
22
+ try {
23
+ stat = await fs.stat(reportPath);
24
+ }
25
+ catch {
26
+ continue;
27
+ }
28
+ if (!stat.isDirectory())
29
+ continue;
30
+ const reportJsonPath = join(reportPath, 'report.json');
31
+ try {
32
+ const reportContent = await fs.readFile(reportJsonPath, 'utf-8');
33
+ const reportData = JSON.parse(reportContent);
34
+ const restored = await restoreFieldsFromFiles(reportPath, reportData);
35
+ const queries = await this.loadLocalQueries(reportPath);
36
+ const specs = await this.loadLocalSpecs(reportPath);
37
+ reports.set(restored.id, {
38
+ main: restored,
39
+ children: { queries, specs },
40
+ path: reportPath
41
+ });
42
+ }
43
+ catch {
44
+ }
45
+ }
46
+ return reports;
47
+ }
48
+ async loadLocalQueries(reportPath) {
49
+ const queries = [];
50
+ const queriesPath = join(reportPath, 'queries');
51
+ let queryFolders = [];
52
+ try {
53
+ queryFolders = await fs.readdir(queriesPath);
54
+ }
55
+ catch {
56
+ return queries;
57
+ }
58
+ for (const folder of queryFolders) {
59
+ const queryPath = join(queriesPath, folder);
60
+ let stat;
61
+ try {
62
+ stat = await fs.stat(queryPath);
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ if (!stat.isDirectory())
68
+ continue;
69
+ const queryJsonPath = join(queryPath, 'query.json');
70
+ try {
71
+ const queryContent = await fs.readFile(queryJsonPath, 'utf-8');
72
+ const queryData = JSON.parse(queryContent);
73
+ const restored = await restoreFieldsFromFiles(queryPath, queryData);
74
+ queries.push(restored);
75
+ }
76
+ catch {
77
+ }
78
+ }
79
+ return queries;
80
+ }
81
+ async loadLocalSpecs(reportPath) {
82
+ const specs = [];
83
+ const specsPath = join(reportPath, 'specs');
84
+ let specFolders = [];
85
+ try {
86
+ specFolders = await fs.readdir(specsPath);
87
+ }
88
+ catch {
89
+ return specs;
90
+ }
91
+ for (const folder of specFolders) {
92
+ const specPath = join(specsPath, folder);
93
+ let stat;
94
+ try {
95
+ stat = await fs.stat(specPath);
96
+ }
97
+ catch {
98
+ continue;
99
+ }
100
+ if (!stat.isDirectory())
101
+ continue;
102
+ const specJsonPath = join(specPath, 'spec.json');
103
+ try {
104
+ const specContent = await fs.readFile(specJsonPath, 'utf-8');
105
+ const specData = JSON.parse(specContent);
106
+ const restored = await restoreFieldsFromFiles(specPath, specData);
107
+ specs.push(restored);
108
+ }
109
+ catch {
110
+ }
111
+ }
112
+ return specs;
113
+ }
114
+ async fetchRemote(api) {
115
+ const reports = new Map();
116
+ const allReports = await api.fetchTable('cms_reports');
117
+ const allSpecs = await api.fetchTable('cms_report_specs');
118
+ const allQueries = await api.fetchTable('cms_report_queries');
119
+ const specsByReportId = new Map();
120
+ for (const spec of allSpecs) {
121
+ const list = specsByReportId.get(spec.cms_report_id) || [];
122
+ list.push(spec);
123
+ specsByReportId.set(spec.cms_report_id, list);
124
+ }
125
+ const queriesByReportId = new Map();
126
+ for (const query of allQueries) {
127
+ const list = queriesByReportId.get(query.cms_report_id) || [];
128
+ list.push(query);
129
+ queriesByReportId.set(query.cms_report_id, list);
130
+ }
131
+ for (const report of allReports) {
132
+ reports.set(report.id, {
133
+ main: report,
134
+ children: {
135
+ specs: specsByReportId.get(report.id) || [],
136
+ queries: queriesByReportId.get(report.id) || []
137
+ }
138
+ });
139
+ }
140
+ return reports;
141
+ }
142
+ compare(local, remote, localPath) {
143
+ const resourceId = local?.main?.id || remote?.main?.id || 0;
144
+ const resourceName = this.getDisplayName(local?.main || remote?.main || {});
145
+ let status;
146
+ if (!local && remote) {
147
+ status = 'remote_only';
148
+ }
149
+ else if (local && !remote) {
150
+ status = 'local_only';
151
+ }
152
+ else if (local && remote) {
153
+ const fieldDiffs = compareFields(local.main, remote.main);
154
+ status = fieldDiffs.length > 0 ? 'modified' : 'unchanged';
155
+ }
156
+ else {
157
+ status = 'unchanged';
158
+ }
159
+ const fieldDiffs = compareFields(local?.main || null, remote?.main || null);
160
+ const queryDiffs = this.compareChildren(local?.children?.queries || [], remote?.children?.queries || [], 'cms_report_queries', (q) => q.name || q.system_name || `query_${q.id}`);
161
+ const specDiffs = this.compareChildren(local?.children?.specs || [], remote?.children?.specs || [], 'cms_report_specs', (s) => s.field_name || `spec_${s.id}`);
162
+ const children = [...queryDiffs, ...specDiffs];
163
+ if (children.length > 0 && status === 'unchanged') {
164
+ status = 'modified';
165
+ }
166
+ return {
167
+ resourceType: this.resourceType,
168
+ resourceName,
169
+ resourceId,
170
+ localPath: localPath || local?.path,
171
+ status,
172
+ fieldDiffs,
173
+ children
174
+ };
175
+ }
176
+ compareChildren(localItems, remoteItems, childType, getDisplayName) {
177
+ const diffs = [];
178
+ const localById = new Map(localItems.map(s => [s.id, s]));
179
+ const remoteById = new Map(remoteItems.map(s => [s.id, s]));
180
+ const allIds = new Set([...localById.keys(), ...remoteById.keys()]);
181
+ for (const itemId of allIds) {
182
+ const localItem = localById.get(itemId);
183
+ const remoteItem = remoteById.get(itemId);
184
+ let status;
185
+ if (!localItem && remoteItem) {
186
+ status = 'remote_only';
187
+ }
188
+ else if (localItem && !remoteItem) {
189
+ status = 'local_only';
190
+ }
191
+ else {
192
+ const fieldDiffs = compareFields(localItem, remoteItem);
193
+ status = fieldDiffs.length > 0 ? 'modified' : 'unchanged';
194
+ }
195
+ if (status !== 'unchanged') {
196
+ const itemName = getDisplayName(localItem || remoteItem);
197
+ diffs.push({
198
+ resourceType: childType,
199
+ resourceName: itemName,
200
+ resourceId: itemId,
201
+ status,
202
+ fieldDiffs: compareFields(localItem || null, remoteItem || null)
203
+ });
204
+ }
205
+ }
206
+ return diffs;
207
+ }
208
+ getDisplayName(data) {
209
+ return data.alias || data.title || `report_${data.id}`;
210
+ }
211
+ }
212
+ //# sourceMappingURL=reportsAdapter.js.map