@slats/claude-assets-sync 0.0.1 → 0.0.3

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 (110) hide show
  1. package/CHANGELOG.md +189 -0
  2. package/README.md +541 -45
  3. package/dist/cli.cjs +8 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.mjs +7 -0
  6. package/dist/commands/add.cjs +61 -0
  7. package/dist/commands/add.d.ts +12 -0
  8. package/dist/commands/add.mjs +59 -0
  9. package/dist/commands/index.d.ts +91 -0
  10. package/dist/commands/list.cjs +83 -0
  11. package/dist/commands/list.d.ts +6 -0
  12. package/dist/commands/list.mjs +81 -0
  13. package/dist/commands/migrate.cjs +9 -0
  14. package/dist/commands/migrate.d.ts +6 -0
  15. package/dist/commands/migrate.mjs +7 -0
  16. package/dist/commands/remove.cjs +109 -0
  17. package/dist/commands/remove.d.ts +6 -0
  18. package/dist/commands/remove.mjs +86 -0
  19. package/dist/commands/status.cjs +193 -0
  20. package/dist/commands/status.d.ts +6 -0
  21. package/dist/commands/status.mjs +171 -0
  22. package/dist/commands/sync.cjs +28 -0
  23. package/dist/commands/sync.d.ts +6 -0
  24. package/dist/commands/sync.mjs +26 -0
  25. package/dist/commands/types.d.ts +82 -0
  26. package/dist/components/add/AddCommand.cjs +92 -0
  27. package/dist/components/add/AddCommand.d.ts +14 -0
  28. package/dist/components/add/AddCommand.mjs +90 -0
  29. package/dist/components/add/index.d.ts +2 -0
  30. package/dist/components/index.d.ts +2 -0
  31. package/dist/components/list/EditableTreeItem.d.ts +13 -0
  32. package/dist/components/list/ListCommand.cjs +440 -0
  33. package/dist/components/list/ListCommand.d.ts +8 -0
  34. package/dist/components/list/ListCommand.mjs +418 -0
  35. package/dist/components/list/SyncedPackageTree.d.ts +14 -0
  36. package/dist/components/list/index.d.ts +9 -0
  37. package/dist/components/primitives/Box.d.ts +4 -0
  38. package/dist/components/primitives/Spinner.d.ts +6 -0
  39. package/dist/components/primitives/Text.d.ts +4 -0
  40. package/dist/components/primitives/index.d.ts +3 -0
  41. package/dist/components/status/PackageStatusCard.d.ts +10 -0
  42. package/dist/components/status/StatusDisplay.cjs +25 -0
  43. package/dist/components/status/StatusDisplay.d.ts +25 -0
  44. package/dist/components/status/StatusDisplay.mjs +23 -0
  45. package/dist/components/status/StatusTreeNode.cjs +40 -0
  46. package/dist/components/status/StatusTreeNode.d.ts +15 -0
  47. package/dist/components/status/StatusTreeNode.mjs +38 -0
  48. package/dist/components/status/index.d.ts +6 -0
  49. package/dist/components/tree/AssetTreeNode.cjs +37 -0
  50. package/dist/components/tree/AssetTreeNode.d.ts +12 -0
  51. package/dist/components/tree/AssetTreeNode.mjs +35 -0
  52. package/dist/components/tree/TreeSelect.cjs +121 -0
  53. package/dist/components/tree/TreeSelect.d.ts +12 -0
  54. package/dist/components/tree/TreeSelect.mjs +119 -0
  55. package/dist/components/tree/index.d.ts +4 -0
  56. package/dist/core/assetStructure.cjs +30 -0
  57. package/dist/core/assetStructure.d.ts +36 -0
  58. package/dist/core/assetStructure.mjs +27 -0
  59. package/dist/core/cli.cjs +92 -0
  60. package/dist/core/cli.mjs +89 -0
  61. package/dist/core/constants.cjs +23 -0
  62. package/dist/core/constants.d.ts +83 -0
  63. package/dist/core/constants.mjs +17 -0
  64. package/dist/core/filesystem.cjs +62 -51
  65. package/dist/core/filesystem.d.ts +38 -18
  66. package/dist/core/filesystem.mjs +56 -44
  67. package/dist/core/github.cjs +8 -11
  68. package/dist/core/github.d.ts +4 -6
  69. package/dist/core/github.mjs +8 -11
  70. package/dist/core/io.cjs +46 -0
  71. package/dist/core/io.d.ts +40 -0
  72. package/dist/core/io.mjs +39 -0
  73. package/dist/core/migration.cjs +199 -0
  74. package/dist/core/migration.d.ts +57 -0
  75. package/dist/core/migration.mjs +196 -0
  76. package/dist/core/packageScanner.cjs +259 -0
  77. package/dist/core/packageScanner.d.ts +17 -0
  78. package/dist/core/packageScanner.mjs +257 -0
  79. package/dist/core/sync.cjs +244 -61
  80. package/dist/core/sync.d.ts +6 -2
  81. package/dist/core/sync.mjs +246 -63
  82. package/dist/core/syncMeta.cjs +93 -0
  83. package/dist/core/syncMeta.d.ts +71 -0
  84. package/dist/core/syncMeta.mjs +84 -0
  85. package/dist/index.cjs +7 -8
  86. package/dist/index.d.ts +4 -3
  87. package/dist/index.mjs +2 -10
  88. package/dist/utils/nameTransform.cjs +12 -0
  89. package/dist/utils/nameTransform.d.ts +76 -0
  90. package/dist/utils/nameTransform.mjs +9 -0
  91. package/dist/utils/package.cjs +22 -17
  92. package/dist/utils/package.d.ts +25 -0
  93. package/dist/utils/package.mjs +11 -7
  94. package/dist/utils/packageName.cjs +24 -0
  95. package/dist/utils/packageName.d.ts +32 -0
  96. package/dist/utils/packageName.mjs +21 -0
  97. package/dist/utils/paths.cjs +18 -0
  98. package/dist/utils/paths.d.ts +55 -0
  99. package/dist/utils/paths.mjs +15 -0
  100. package/dist/utils/types.d.ts +153 -6
  101. package/dist/utils/version.cjs +17 -0
  102. package/dist/utils/version.d.ts +55 -0
  103. package/dist/utils/version.mjs +14 -0
  104. package/dist/version.cjs +5 -0
  105. package/dist/version.d.ts +5 -0
  106. package/dist/version.mjs +3 -0
  107. package/package.json +16 -7
  108. package/dist/cli/index.cjs +0 -56
  109. package/dist/cli/index.mjs +0 -53
  110. /package/dist/{cli/index.d.ts → core/cli.d.ts} +0 -0
@@ -0,0 +1,90 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useHandle } from '@winglet/react-utils/hook';
3
+ import { Box, Text } from 'ink';
4
+ import Spinner from 'ink-spinner';
5
+ import { useState, useEffect, useCallback } from 'react';
6
+ import { scanPackageAssets } from '../../core/packageScanner.mjs';
7
+ import { TreeSelect } from '../tree/TreeSelect.mjs';
8
+
9
+ const AddCommand = ({ packageName, local, ref, onComplete, onError, onCancel, }) => {
10
+ const [state, setState] = useState('scanning');
11
+ const [trees, setTrees] = useState([]);
12
+ const [error, setError] = useState(null);
13
+ const handleComplete = useHandle(onComplete);
14
+ const handleError = useHandle(onError);
15
+ const handleCancel = useHandle(onCancel);
16
+ useEffect(() => {
17
+ const scan = async () => {
18
+ try {
19
+ setState('scanning');
20
+ const scanned = await scanPackageAssets(packageName, { local, ref });
21
+ setTrees(scanned);
22
+ setState('selecting');
23
+ }
24
+ catch (err) {
25
+ const error = err instanceof Error ? err : new Error(String(err));
26
+ setError(error);
27
+ setState('error');
28
+ handleError(error);
29
+ }
30
+ };
31
+ scan();
32
+ }, [packageName, local, ref, handleError]);
33
+ const handleSubmit = useCallback((selectedTrees) => {
34
+ setState('confirming');
35
+ const includedAssets = {};
36
+ const excludedAssets = {};
37
+ for (const tree of selectedTrees) {
38
+ const assetType = tree.label;
39
+ includedAssets[assetType] = [];
40
+ excludedAssets[assetType] = [];
41
+ if (tree.children) {
42
+ extractSelection(tree.children, includedAssets[assetType], excludedAssets[assetType]);
43
+ }
44
+ }
45
+ const selection = {
46
+ packageName,
47
+ source: local ? 'local' : 'npm',
48
+ ref,
49
+ includedAssets,
50
+ excludedAssets,
51
+ };
52
+ setState('done');
53
+ handleComplete(selection);
54
+ }, [packageName, local, ref, handleComplete]);
55
+ if (state === 'scanning') {
56
+ return (jsx(Box, { children: jsxs(Text, { color: "cyan", children: [jsx(Spinner, { type: "dots" }), " Scanning package assets..."] }) }));
57
+ }
58
+ if (state === 'error') {
59
+ return (jsx(Box, { flexDirection: "column", children: jsxs(Text, { color: "red", children: ["Error: ", error?.message] }) }));
60
+ }
61
+ if (state === 'selecting') {
62
+ return (jsxs(Box, { flexDirection: "column", children: [jsx(Box, { marginBottom: 1, children: jsxs(Text, { bold: true, children: ["Select assets to sync from ", packageName] }) }), jsx(Box, { marginBottom: 1, children: jsx(Text, { dimColor: true, children: "Use \u2191/\u2193 to navigate, Space to toggle, \u2192/\u2190 to expand/collapse, Enter to confirm, Esc to cancel" }) }), jsx(TreeSelect, { trees: trees, onSubmit: handleSubmit, onCancel: handleCancel })] }));
63
+ }
64
+ if (state === 'confirming' || state === 'done') {
65
+ return (jsx(Box, { children: jsx(Text, { color: "green", children: "\u2713 Selection confirmed" }) }));
66
+ }
67
+ return null;
68
+ };
69
+ function extractSelection(nodes, included, excluded) {
70
+ for (const node of nodes) {
71
+ if (node.selected) {
72
+ included.push(node.path);
73
+ }
74
+ else {
75
+ excluded.push(node.path);
76
+ }
77
+ if (node.children && hasPartialSelection(node)) {
78
+ extractSelection(node.children, included, excluded);
79
+ }
80
+ }
81
+ }
82
+ function hasPartialSelection(node) {
83
+ if (!node.children || node.children.length === 0) {
84
+ return false;
85
+ }
86
+ const selectedCount = node.children.filter((c) => c.selected).length;
87
+ return selectedCount > 0 && selectedCount < node.children.length;
88
+ }
89
+
90
+ export { AddCommand };
@@ -0,0 +1,2 @@
1
+ export { AddCommand } from './AddCommand.js';
2
+ export type { AddCommandProps } from './AddCommand.js';
@@ -0,0 +1,2 @@
1
+ export * from './primitives/index.js';
2
+ export * from './status/index.js';
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ export interface EditableTreeItemProps {
3
+ label: string;
4
+ depth: number;
5
+ selected: boolean;
6
+ markedForDeletion?: boolean;
7
+ markedForAddition?: boolean;
8
+ onToggleDelete?: () => void;
9
+ }
10
+ /**
11
+ * Editable tree item component for list view
12
+ */
13
+ export declare const EditableTreeItem: React.FC<EditableTreeItemProps>;
@@ -0,0 +1,440 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var fs = require('node:fs');
5
+ var path = require('node:path');
6
+ var ink = require('ink');
7
+ var Spinner = require('ink-spinner');
8
+ var React = require('react');
9
+ var packageScanner = require('../../core/packageScanner.cjs');
10
+ var sync = require('../../core/sync.cjs');
11
+ var syncMeta = require('../../core/syncMeta.cjs');
12
+ var TreeSelect = require('../tree/TreeSelect.cjs');
13
+
14
+ function _interopNamespaceDefault(e) {
15
+ var n = Object.create(null);
16
+ if (e) {
17
+ Object.keys(e).forEach(function (k) {
18
+ if (k !== 'default') {
19
+ var d = Object.getOwnPropertyDescriptor(e, k);
20
+ Object.defineProperty(n, k, d.get ? d : {
21
+ enumerable: true,
22
+ get: function () { return e[k]; }
23
+ });
24
+ }
25
+ });
26
+ }
27
+ n.default = e;
28
+ return Object.freeze(n);
29
+ }
30
+
31
+ var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
32
+ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
33
+
34
+ const ListCommand = ({ cwd }) => {
35
+ const [state, setState] = React.useState('loading');
36
+ const [packageFiles, setPackageFiles] = React.useState({});
37
+ const [meta, setMeta] = React.useState(null);
38
+ const [lastRefresh, setLastRefresh] = React.useState(Date.now());
39
+ const [changesSummary, setChangesSummary] = React.useState(null);
40
+ const hasLoadedRef = React.useRef(false);
41
+ const buildTrees = () => {
42
+ if (!meta)
43
+ return [];
44
+ const trees = [];
45
+ for (const [prefix, packageInfo] of Object.entries(meta.packages)) {
46
+ const packageData = packageFiles[prefix];
47
+ if (!packageData)
48
+ continue;
49
+ const assetTypeTrees = packageData.available.map((assetTypeTree) => {
50
+ const clonedTree = { ...assetTypeTree };
51
+ if (clonedTree.children) {
52
+ clonedTree.children = clonedTree.children.map((fileNode) => {
53
+ const filePath = `${assetTypeTree.label}/${fileNode.label}`;
54
+ return {
55
+ ...fileNode,
56
+ selected: packageData.selected.has(filePath),
57
+ };
58
+ });
59
+ const anySelected = clonedTree.children.some((f) => f.selected);
60
+ clonedTree.selected = anySelected;
61
+ }
62
+ return clonedTree;
63
+ });
64
+ trees.push({
65
+ id: prefix,
66
+ label: `${packageInfo.originalName}@${packageInfo.version}`,
67
+ path: packageInfo.originalName,
68
+ type: 'directory',
69
+ children: assetTypeTrees,
70
+ selected: true,
71
+ expanded: true,
72
+ });
73
+ }
74
+ return trees;
75
+ };
76
+ const loadSyncedFiles = React.useCallback(async (isRefresh = false) => {
77
+ const loadedMeta = syncMeta.readUnifiedSyncMeta(cwd);
78
+ if (!loadedMeta || Object.keys(loadedMeta.packages).length === 0) {
79
+ setState('done');
80
+ return;
81
+ }
82
+ if (!isRefresh) {
83
+ setMeta(loadedMeta);
84
+ }
85
+ const currentSelected = {};
86
+ setPackageFiles((prev) => {
87
+ for (const [prefix, data] of Object.entries(prev)) {
88
+ currentSelected[prefix] = data.selected;
89
+ }
90
+ return prev;
91
+ });
92
+ const fetchPromises = Object.entries(loadedMeta.packages).map(async ([prefix, packageInfo]) => {
93
+ try {
94
+ const isLocal = packageInfo.local ?? true;
95
+ let scannedTrees = await packageScanner.scanPackageAssets(packageInfo.originalName, {
96
+ local: isLocal,
97
+ ref: undefined,
98
+ }).catch(() => null);
99
+ if (!scannedTrees && packageInfo.local === undefined) {
100
+ scannedTrees = await packageScanner.scanPackageAssets(packageInfo.originalName, {
101
+ local: !isLocal,
102
+ ref: undefined,
103
+ }).catch(() => null);
104
+ }
105
+ if (scannedTrees) {
106
+ return { prefix, available: scannedTrees, packageInfo };
107
+ }
108
+ else {
109
+ throw new Error('Scan failed, using synced files only');
110
+ }
111
+ }
112
+ catch {
113
+ const assetTypeTrees = [];
114
+ for (const [assetType, files] of Object.entries(packageInfo.files)) {
115
+ const fileArray = Array.isArray(files) ? files : [];
116
+ if (fileArray.length === 0)
117
+ continue;
118
+ const fileNodes = fileArray.map((file) => {
119
+ const fileName = typeof file === 'string' ? file : file.transformed;
120
+ return {
121
+ id: `${prefix}/${assetType}/${fileName}`,
122
+ label: fileName,
123
+ path: `${assetType}/${fileName}`,
124
+ type: 'file',
125
+ selected: true,
126
+ expanded: false,
127
+ };
128
+ });
129
+ assetTypeTrees.push({
130
+ id: `${prefix}/${assetType}`,
131
+ label: assetType,
132
+ path: assetType,
133
+ type: 'directory',
134
+ children: fileNodes,
135
+ selected: true,
136
+ expanded: true,
137
+ });
138
+ }
139
+ return { prefix, available: assetTypeTrees, packageInfo };
140
+ }
141
+ });
142
+ const results = await Promise.all(fetchPromises);
143
+ const newPackageFiles = {};
144
+ for (const { prefix, available, packageInfo } of results) {
145
+ const excludedFiles = new Set(packageInfo.exclusions?.files || []);
146
+ const metaSelectedFiles = new Set(Object.entries(packageInfo.files).flatMap(([assetType, files]) => (Array.isArray(files) ? files : [])
147
+ .map((f) => {
148
+ const fileName = typeof f === 'string' ? f : f.original;
149
+ const filePath = `${assetType}/${fileName}`;
150
+ if (excludedFiles.has(filePath) ||
151
+ excludedFiles.has(fileName)) {
152
+ return null;
153
+ }
154
+ return filePath;
155
+ })
156
+ .filter((f) => f !== null)));
157
+ const selectedFiles = isRefresh && currentSelected[prefix]
158
+ ? currentSelected[prefix]
159
+ : metaSelectedFiles;
160
+ newPackageFiles[prefix] = {
161
+ available,
162
+ selected: selectedFiles,
163
+ };
164
+ }
165
+ setPackageFiles(newPackageFiles);
166
+ setState('selecting');
167
+ }, [cwd]);
168
+ React.useEffect(() => {
169
+ if (!hasLoadedRef.current) {
170
+ hasLoadedRef.current = true;
171
+ loadSyncedFiles().catch((error) => {
172
+ console.error('Failed to load synced files:', error);
173
+ setState('done');
174
+ });
175
+ }
176
+ }, [loadSyncedFiles]);
177
+ const handleRefresh = () => {
178
+ if (state !== 'selecting')
179
+ return;
180
+ const now = Date.now();
181
+ if (now - lastRefresh < 30000) {
182
+ return;
183
+ }
184
+ setLastRefresh(now);
185
+ setState('loading');
186
+ setTimeout(() => {
187
+ loadSyncedFiles(true).catch((error) => {
188
+ console.error('Failed to refresh:', error);
189
+ setState('selecting');
190
+ });
191
+ }, 100);
192
+ };
193
+ ink.useInput((input) => {
194
+ if (state === 'confirming') {
195
+ if (input === 'y' || input === 'Y') {
196
+ handleConfirm();
197
+ }
198
+ else if (input === 'n' || input === 'N') {
199
+ handleCancelConfirm();
200
+ }
201
+ }
202
+ });
203
+ const handleSubmit = async (selectedTrees) => {
204
+ if (!meta) {
205
+ setState('done');
206
+ process.exit(0);
207
+ return;
208
+ }
209
+ const newPackageFiles = { ...packageFiles };
210
+ for (const packageTree of selectedTrees) {
211
+ const prefix = packageTree.id;
212
+ if (!newPackageFiles[prefix])
213
+ continue;
214
+ const selectedPaths = new Set();
215
+ if (packageTree.children) {
216
+ for (const assetTypeNode of packageTree.children) {
217
+ if (assetTypeNode.children) {
218
+ for (const fileNode of assetTypeNode.children) {
219
+ if (fileNode.selected) {
220
+ selectedPaths.add(`${assetTypeNode.label}/${fileNode.label}`);
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ newPackageFiles[prefix] = {
227
+ ...newPackageFiles[prefix],
228
+ selected: selectedPaths,
229
+ };
230
+ }
231
+ setPackageFiles(newPackageFiles);
232
+ const fileOperations = [];
233
+ const filesToDelete = [];
234
+ const packagesToSync = [];
235
+ for (const [prefix] of Object.entries(meta.packages)) {
236
+ const packageTree = selectedTrees.find((t) => t.id === prefix);
237
+ const packageInfo = meta.packages[prefix];
238
+ const packageData = newPackageFiles[prefix];
239
+ if (!packageTree || !packageTree.selected || !packageData) {
240
+ for (const [assetType, files] of Object.entries(packageInfo.files)) {
241
+ if (!Array.isArray(files))
242
+ continue;
243
+ const firstFile = files[0];
244
+ const isFlat = typeof firstFile === 'object' && 'transformed' in firstFile;
245
+ for (const file of files) {
246
+ const fileName = typeof file === 'string' ? file : file.original;
247
+ fileOperations.push({
248
+ type: 'remove',
249
+ prefix,
250
+ assetType,
251
+ fileName,
252
+ });
253
+ }
254
+ if (isFlat) {
255
+ for (const file of files) {
256
+ const filePath = path__namespace.join(cwd, '.claude', assetType, file.transformed);
257
+ filesToDelete.push(filePath);
258
+ }
259
+ }
260
+ else {
261
+ const dirPath = path__namespace.join(cwd, '.claude', assetType, packageInfo.originalName);
262
+ filesToDelete.push(dirPath);
263
+ }
264
+ }
265
+ continue;
266
+ }
267
+ let hasChanges = false;
268
+ const selectedByAssetType = {};
269
+ for (const filePath of packageData.selected) {
270
+ const [assetType, fileName] = filePath.split('/');
271
+ if (!selectedByAssetType[assetType]) {
272
+ selectedByAssetType[assetType] = new Set();
273
+ }
274
+ selectedByAssetType[assetType].add(fileName);
275
+ }
276
+ for (const [assetType, originalFiles] of Object.entries(packageInfo.files)) {
277
+ const selectedFiles = selectedByAssetType[assetType];
278
+ const fileArray = Array.isArray(originalFiles) ? originalFiles : [];
279
+ const firstFile = fileArray[0];
280
+ const isFlat = typeof firstFile === 'object' && 'transformed' in firstFile;
281
+ if (!selectedFiles || selectedFiles.size === 0) {
282
+ hasChanges = true;
283
+ for (const file of fileArray) {
284
+ const fileName = typeof file === 'string' ? file : file.original;
285
+ fileOperations.push({
286
+ type: 'remove',
287
+ prefix,
288
+ assetType,
289
+ fileName,
290
+ });
291
+ }
292
+ if (isFlat) {
293
+ for (const file of fileArray) {
294
+ const filePath = path__namespace.join(cwd, '.claude', assetType, file.transformed);
295
+ filesToDelete.push(filePath);
296
+ }
297
+ }
298
+ else {
299
+ const dirPath = path__namespace.join(cwd, '.claude', assetType, packageInfo.originalName);
300
+ filesToDelete.push(dirPath);
301
+ }
302
+ continue;
303
+ }
304
+ const originalFileNames = new Set(fileArray.map((f) => (typeof f === 'string' ? f : f.transformed)));
305
+ for (const file of fileArray) {
306
+ const fileName = typeof file === 'string' ? file : file.original;
307
+ if (!selectedFiles.has(fileName)) {
308
+ hasChanges = true;
309
+ fileOperations.push({
310
+ type: 'remove',
311
+ prefix,
312
+ assetType,
313
+ fileName,
314
+ });
315
+ if (isFlat) {
316
+ const filePath = path__namespace.join(cwd, '.claude', assetType, file.transformed);
317
+ filesToDelete.push(filePath);
318
+ }
319
+ else {
320
+ const filePath = path__namespace.join(cwd, '.claude', assetType, packageInfo.originalName, fileName);
321
+ filesToDelete.push(filePath);
322
+ }
323
+ }
324
+ }
325
+ for (const fileName of selectedFiles) {
326
+ if (!originalFileNames.has(fileName)) {
327
+ hasChanges = true;
328
+ fileOperations.push({
329
+ type: 'add',
330
+ prefix,
331
+ assetType,
332
+ fileName,
333
+ });
334
+ }
335
+ }
336
+ }
337
+ if (hasChanges) {
338
+ packagesToSync.push({
339
+ prefix,
340
+ name: packageInfo.originalName,
341
+ local: packageInfo.local,
342
+ });
343
+ }
344
+ }
345
+ setChangesSummary({ filesToDelete, fileOperations, packagesToSync });
346
+ setState('confirming');
347
+ };
348
+ const handleConfirm = async () => {
349
+ if (!changesSummary || !meta)
350
+ return;
351
+ setState('saving');
352
+ for (const filePath of changesSummary.filesToDelete) {
353
+ try {
354
+ const stat = fs__namespace.statSync(filePath);
355
+ if (stat.isDirectory()) {
356
+ fs__namespace.rmSync(filePath, { recursive: true, force: true });
357
+ }
358
+ else {
359
+ fs__namespace.unlinkSync(filePath);
360
+ }
361
+ }
362
+ catch (error) {
363
+ if (error.code !== 'ENOENT') {
364
+ console.error(`Failed to remove ${filePath}: ${error}`);
365
+ }
366
+ }
367
+ }
368
+ let updatedMeta = { ...meta };
369
+ const hasAddOperations = changesSummary.fileOperations.some((op) => op.type === 'add');
370
+ for (const op of changesSummary.fileOperations) {
371
+ if (op.type === 'remove') {
372
+ updatedMeta = syncMeta.removeFileFromPackage(updatedMeta, op.prefix, op.assetType, op.fileName);
373
+ }
374
+ }
375
+ for (const prefix of Object.keys(updatedMeta.packages)) {
376
+ const pkg = updatedMeta.packages[prefix];
377
+ if (!pkg.files || Object.keys(pkg.files).length === 0) {
378
+ updatedMeta = syncMeta.removePackageFromMeta(updatedMeta, prefix);
379
+ }
380
+ }
381
+ syncMeta.writeUnifiedSyncMeta(cwd, updatedMeta);
382
+ if (hasAddOperations && changesSummary.packagesToSync.length > 0) {
383
+ setState('syncing');
384
+ for (const { prefix, name, local } of changesSummary.packagesToSync) {
385
+ try {
386
+ const removedFiles = changesSummary.fileOperations
387
+ .filter((op) => op.type === 'remove' && op.prefix === prefix)
388
+ .map((op) => `${op.assetType}/${op.fileName}`);
389
+ const exclusions = removedFiles.length > 0
390
+ ? { directories: [], files: removedFiles }
391
+ : undefined;
392
+ await sync.syncPackage(name, {
393
+ force: true,
394
+ dryRun: false,
395
+ local: local ?? false,
396
+ ref: undefined,
397
+ flat: undefined,
398
+ }, cwd, exclusions);
399
+ }
400
+ catch (error) {
401
+ console.error(`Failed to sync ${name}: ${error}`);
402
+ }
403
+ }
404
+ }
405
+ setState('done');
406
+ setTimeout(() => process.exit(0), 500);
407
+ };
408
+ const handleCancelConfirm = () => {
409
+ setChangesSummary(null);
410
+ setState('selecting');
411
+ };
412
+ const handleCancel = () => {
413
+ setState('done');
414
+ process.exit(0);
415
+ };
416
+ if (state === 'loading') {
417
+ return (jsxRuntime.jsx(ink.Box, { children: jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [jsxRuntime.jsx(Spinner, { type: "dots" }), " Loading synced packages..."] }) }));
418
+ }
419
+ if (state === 'confirming' && changesSummary) {
420
+ return (jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: jsxRuntime.jsx(ink.Text, { bold: true, children: "Changes to be applied:" }) }), changesSummary.filesToDelete.length > 0 && (jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginBottom: 1, children: [jsxRuntime.jsxs(ink.Text, { color: "red", bold: true, children: ["Files to delete (", changesSummary.filesToDelete.length, "):"] }), changesSummary.filesToDelete.slice(0, 10).map((filePath) => (jsxRuntime.jsx(ink.Box, { marginLeft: 2, children: jsxRuntime.jsxs(ink.Text, { color: "red", children: ["- ", filePath.replace(cwd, '.')] }) }, filePath))), changesSummary.filesToDelete.length > 10 && (jsxRuntime.jsx(ink.Box, { marginLeft: 2, children: jsxRuntime.jsxs(ink.Text, { dimColor: true, children: ["... and ", changesSummary.filesToDelete.length - 10, " more"] }) }))] })), changesSummary.packagesToSync.length > 0 && (jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginBottom: 1, children: [jsxRuntime.jsxs(ink.Text, { color: "green", bold: true, children: ["Packages to sync (", changesSummary.packagesToSync.length, "):"] }), changesSummary.packagesToSync.map(({ name }) => (jsxRuntime.jsx(ink.Box, { marginLeft: 2, children: jsxRuntime.jsxs(ink.Text, { color: "green", children: ["+ ", name] }) }, name)))] })), changesSummary.filesToDelete.length === 0 &&
421
+ changesSummary.packagesToSync.length === 0 && (jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: jsxRuntime.jsx(ink.Text, { dimColor: true, children: "No changes to apply" }) })), jsxRuntime.jsx(ink.Box, { marginTop: 1, children: jsxRuntime.jsx(ink.Text, { bold: true, children: "Apply changes and sync? (y/n): " }) })] }));
422
+ }
423
+ if (state === 'saving') {
424
+ return (jsxRuntime.jsx(ink.Box, { children: jsxRuntime.jsxs(ink.Text, { color: "green", children: [jsxRuntime.jsx(Spinner, { type: "dots" }), " Saving changes..."] }) }));
425
+ }
426
+ if (state === 'syncing') {
427
+ return (jsxRuntime.jsx(ink.Box, { children: jsxRuntime.jsxs(ink.Text, { color: "cyan", children: [jsxRuntime.jsx(Spinner, { type: "dots" }), " Syncing packages..."] }) }));
428
+ }
429
+ if (state === 'done' &&
430
+ (!meta || Object.keys(meta?.packages || {}).length === 0)) {
431
+ return (jsxRuntime.jsx(ink.Box, { children: jsxRuntime.jsx(ink.Text, { color: "yellow", children: "No packages synced yet." }) }));
432
+ }
433
+ if (state === 'selecting') {
434
+ const trees = buildTrees();
435
+ return (jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: jsxRuntime.jsx(ink.Text, { bold: true, children: "Edit synced packages" }) }), jsxRuntime.jsx(ink.Box, { marginBottom: 1, children: jsxRuntime.jsx(ink.Text, { dimColor: true, children: "\u2191/\u2193: navigate \u2022 Space: toggle \u2022 \u2192/\u2190: expand/collapse \u2022 r: refresh (30s cooldown) \u2022 Enter: save \u2022 Esc: cancel" }) }), jsxRuntime.jsx(TreeSelect.TreeSelect, { trees: trees, onSubmit: handleSubmit, onCancel: handleCancel, onRefresh: handleRefresh })] }));
436
+ }
437
+ return (jsxRuntime.jsx(ink.Box, { children: jsxRuntime.jsx(ink.Text, { color: "green", children: "\u2713 Changes saved" }) }));
438
+ };
439
+
440
+ exports.ListCommand = ListCommand;
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ export interface ListCommandProps {
3
+ cwd: string;
4
+ }
5
+ /**
6
+ * List command component - interactive tree-based editing of synced files
7
+ */
8
+ export declare const ListCommand: React.FC<ListCommandProps>;