@koder-0x/scopy 1.0.0

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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/commands/config.d.ts +3 -0
  4. package/dist/commands/config.d.ts.map +1 -0
  5. package/dist/commands/config.js +51 -0
  6. package/dist/commands/config.js.map +1 -0
  7. package/dist/commands/dest.d.ts +3 -0
  8. package/dist/commands/dest.d.ts.map +1 -0
  9. package/dist/commands/dest.js +54 -0
  10. package/dist/commands/dest.js.map +1 -0
  11. package/dist/commands/ghost.d.ts +4 -0
  12. package/dist/commands/ghost.d.ts.map +1 -0
  13. package/dist/commands/ghost.js +138 -0
  14. package/dist/commands/ghost.js.map +1 -0
  15. package/dist/commands/info.d.ts +4 -0
  16. package/dist/commands/info.d.ts.map +1 -0
  17. package/dist/commands/info.js +23 -0
  18. package/dist/commands/info.js.map +1 -0
  19. package/dist/commands/list.d.ts +3 -0
  20. package/dist/commands/list.d.ts.map +1 -0
  21. package/dist/commands/list.js +27 -0
  22. package/dist/commands/list.js.map +1 -0
  23. package/dist/commands/log.d.ts +3 -0
  24. package/dist/commands/log.d.ts.map +1 -0
  25. package/dist/commands/log.js +62 -0
  26. package/dist/commands/log.js.map +1 -0
  27. package/dist/commands/purge.d.ts +9 -0
  28. package/dist/commands/purge.d.ts.map +1 -0
  29. package/dist/commands/purge.js +62 -0
  30. package/dist/commands/purge.js.map +1 -0
  31. package/dist/commands/resync.d.ts +8 -0
  32. package/dist/commands/resync.d.ts.map +1 -0
  33. package/dist/commands/resync.js +218 -0
  34. package/dist/commands/resync.js.map +1 -0
  35. package/dist/commands/source.d.ts +3 -0
  36. package/dist/commands/source.d.ts.map +1 -0
  37. package/dist/commands/source.js +80 -0
  38. package/dist/commands/source.js.map +1 -0
  39. package/dist/commands/sync.d.ts +14 -0
  40. package/dist/commands/sync.d.ts.map +1 -0
  41. package/dist/commands/sync.js +288 -0
  42. package/dist/commands/sync.js.map +1 -0
  43. package/dist/config.d.ts +29 -0
  44. package/dist/config.d.ts.map +1 -0
  45. package/dist/config.js +164 -0
  46. package/dist/config.js.map +1 -0
  47. package/dist/glob.d.ts +2 -0
  48. package/dist/glob.d.ts.map +1 -0
  49. package/dist/glob.js +5 -0
  50. package/dist/glob.js.map +1 -0
  51. package/dist/scopy.d.ts +3 -0
  52. package/dist/scopy.d.ts.map +1 -0
  53. package/dist/scopy.js +37 -0
  54. package/dist/scopy.js.map +1 -0
  55. package/dist/types.d.ts +47 -0
  56. package/dist/types.d.ts.map +1 -0
  57. package/dist/types.js +7 -0
  58. package/dist/types.js.map +1 -0
  59. package/dist/ui.d.ts +12 -0
  60. package/dist/ui.d.ts.map +1 -0
  61. package/dist/ui.js +37 -0
  62. package/dist/ui.js.map +1 -0
  63. package/dist/validate.d.ts +6 -0
  64. package/dist/validate.d.ts.map +1 -0
  65. package/dist/validate.js +13 -0
  66. package/dist/validate.js.map +1 -0
  67. package/package.json +51 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"purge.js","sourceRoot":"","sources":["../../src/commands/purge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Command } from 'commander';
2
+ export interface ResyncOptions {
3
+ dryRun: boolean;
4
+ unghost: boolean;
5
+ }
6
+ export declare function handleResync(dest: string, opts: ResyncOptions): Promise<void>;
7
+ export default function registerResync(program: Command): void;
8
+ //# sourceMappingURL=resync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resync.d.ts","sourceRoot":"","sources":["../../src/commands/resync.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAczC,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA+NnF;AAED,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAO7D"}
@@ -0,0 +1,218 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { getSources, getDestinations, destinationExists, addCopy, getCopiesByDestination, fileCacheDir, fileCachePath, setGhosted, } from '../config.js';
5
+ import { error as uiError, dim } from '../ui.js';
6
+ export async function handleResync(dest, opts) {
7
+ const dryRun = opts.dryRun;
8
+ if (!destinationExists(dest)) {
9
+ uiError(`Destination "${dest}" is not registered`);
10
+ return;
11
+ }
12
+ const destinations = getDestinations();
13
+ const destination = destinations.find((d) => d.name === dest);
14
+ if (destination === undefined) {
15
+ uiError(`Destination "${dest}" is not registered`);
16
+ return;
17
+ }
18
+ const records = getCopiesByDestination(dest);
19
+ if (records.length === 0) {
20
+ dim(`No tracked files for "${dest}"`);
21
+ return;
22
+ }
23
+ const ghostedRecords = records.filter((r) => r.ghosted);
24
+ const activeRecords = records.filter((r) => !r.ghosted);
25
+ // --unghost: restore ghosted files from cache
26
+ if (opts.unghost) {
27
+ if (ghostedRecords.length === 0) {
28
+ dim('No ghosted files to restore');
29
+ return;
30
+ }
31
+ if (dryRun) {
32
+ console.log(`Would restore ${ghostedRecords.length} ghosted file(s):`);
33
+ for (const record of ghostedRecords) {
34
+ console.log(chalk.dim(`· ${record.file}`));
35
+ }
36
+ return;
37
+ }
38
+ let restored = 0;
39
+ let errs = 0;
40
+ for (const record of ghostedRecords) {
41
+ if (record.index === undefined) {
42
+ uiError(`${record.file}: missing index, cannot restore`);
43
+ errs++;
44
+ continue;
45
+ }
46
+ const cachePath = fileCachePath(dest, record.index);
47
+ if (!fs.existsSync(cachePath)) {
48
+ uiError(`${record.file}: cache not found, cannot restore`);
49
+ errs++;
50
+ continue;
51
+ }
52
+ const destPath = path.join(destination.location, record.file);
53
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
54
+ fs.copyFileSync(cachePath, destPath);
55
+ setGhosted(dest, record.index, false);
56
+ addCopy({
57
+ source: record.source,
58
+ destination: dest,
59
+ file: record.file,
60
+ copiedAt: new Date().toISOString(),
61
+ });
62
+ console.log(`${chalk.green('✓')} ${record.file} (restored from cache)`);
63
+ restored++;
64
+ }
65
+ const restoredStr = chalk.green(String(restored));
66
+ const errStr = errs > 0 ? chalk.red(`${errs} error(s)`) : `${errs} error(s)`;
67
+ console.log(`${restoredStr} restored, ${errStr}`);
68
+ if (errs > 0)
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+ // Group records by source name
73
+ const bySource = new Map();
74
+ for (const record of activeRecords) {
75
+ const group = bySource.get(record.source);
76
+ if (group !== undefined) {
77
+ group.push(record);
78
+ }
79
+ else {
80
+ bySource.set(record.source, [record]);
81
+ }
82
+ }
83
+ const sources = getSources();
84
+ if (dryRun) {
85
+ // Collect all valid files first, then list
86
+ const validFiles = [];
87
+ for (const [sourceName, group] of bySource) {
88
+ const source = sources.find((s) => s.name === sourceName);
89
+ if (source === undefined) {
90
+ for (const record of group) {
91
+ uiError(`${record.file}: source "${sourceName}" is no longer registered`);
92
+ }
93
+ continue;
94
+ }
95
+ if (source.type === 'git') {
96
+ for (const record of group) {
97
+ validFiles.push(record.file);
98
+ }
99
+ continue;
100
+ }
101
+ const workTree = source.location;
102
+ for (const record of group) {
103
+ const srcPath = path.join(workTree, record.sourcePath ?? record.file);
104
+ if (!fs.existsSync(srcPath)) {
105
+ uiError(`${record.file}: file not found in source`);
106
+ continue;
107
+ }
108
+ validFiles.push(record.file);
109
+ }
110
+ }
111
+ console.log(`Would copy ${validFiles.length} file(s)`);
112
+ for (const file of validFiles) {
113
+ console.log(chalk.dim(`· ${file}`));
114
+ }
115
+ return;
116
+ }
117
+ let copied = 0;
118
+ let errors = 0;
119
+ for (const [sourceName, group] of bySource) {
120
+ const source = sources.find((s) => s.name === sourceName);
121
+ if (source === undefined) {
122
+ for (const record of group) {
123
+ uiError(`${record.file}: source "${sourceName}" is no longer registered`);
124
+ errors++;
125
+ }
126
+ continue;
127
+ }
128
+ if (source.type === 'git') {
129
+ // Parse owner/repo from stored location (https://github.com/{owner}/{repo})
130
+ const urlParts = new URL(source.location).pathname.split('/').filter(Boolean);
131
+ const owner = urlParts[0];
132
+ const repo = urlParts[1];
133
+ const subPath = source.path ? source.path.replace(/^\//, '') : '';
134
+ const headers = { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' };
135
+ for (const record of group) {
136
+ const fileRelPath = record.sourcePath ?? record.file;
137
+ const filePath = subPath ? `${subPath}/${fileRelPath}` : fileRelPath;
138
+ try {
139
+ const metaRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${filePath}`, { headers });
140
+ if (!metaRes.ok) {
141
+ uiError(`${record.file}: HTTP ${metaRes.status}`);
142
+ errors++;
143
+ continue;
144
+ }
145
+ const meta = await metaRes.json();
146
+ if (typeof meta.download_url !== 'string') {
147
+ uiError(`${record.file}: no download_url in API response`);
148
+ errors++;
149
+ continue;
150
+ }
151
+ const res = await fetch(meta.download_url);
152
+ if (!res.ok) {
153
+ uiError(`${record.file}: HTTP ${res.status}`);
154
+ errors++;
155
+ continue;
156
+ }
157
+ const content = Buffer.from(await res.arrayBuffer());
158
+ const destPath = path.join(destination.location, record.file);
159
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
160
+ fs.writeFileSync(destPath, content);
161
+ if (record.index !== undefined) {
162
+ fs.mkdirSync(fileCacheDir(dest), { recursive: true });
163
+ fs.writeFileSync(fileCachePath(dest, record.index), content);
164
+ }
165
+ const resolvedSourcePath = subPath && filePath.startsWith(subPath + '/') ? filePath.slice(subPath.length + 1) : filePath;
166
+ addCopy({ source: sourceName, destination: dest, file: record.file, sourcePath: resolvedSourcePath, copiedAt: new Date().toISOString() });
167
+ console.log(`${chalk.green('✓')} ${record.file}`);
168
+ copied++;
169
+ }
170
+ catch (err) {
171
+ uiError(`${record.file}: ${err instanceof Error ? err.message : String(err)}`);
172
+ errors++;
173
+ }
174
+ }
175
+ continue; // next source group
176
+ }
177
+ // Local source
178
+ const workTree = source.location;
179
+ for (const record of group) {
180
+ const srcPath = path.join(workTree, record.sourcePath ?? record.file);
181
+ if (!fs.existsSync(srcPath)) {
182
+ uiError(`${record.file}: file not found in source`);
183
+ errors++;
184
+ continue;
185
+ }
186
+ const destPath = path.join(destination.location, record.file);
187
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
188
+ fs.copyFileSync(srcPath, destPath);
189
+ if (record.index !== undefined) {
190
+ fs.mkdirSync(fileCacheDir(dest), { recursive: true });
191
+ fs.copyFileSync(srcPath, fileCachePath(dest, record.index));
192
+ }
193
+ addCopy({
194
+ source: sourceName,
195
+ destination: dest,
196
+ file: record.file,
197
+ copiedAt: new Date().toISOString(),
198
+ });
199
+ console.log(`${chalk.green('✓')} ${record.file}`);
200
+ copied++;
201
+ }
202
+ }
203
+ const copiedStr = chalk.green(String(copied));
204
+ const errorStr = errors > 0 ? chalk.red(`${errors} error(s)`) : `${errors} error(s)`;
205
+ console.log(`${copiedStr} copied, ${errorStr}`);
206
+ if (errors > 0) {
207
+ process.exitCode = 1;
208
+ }
209
+ }
210
+ export default function registerResync(program) {
211
+ program
212
+ .command('resync <dest>')
213
+ .description('Re-copy all tracked files to a destination')
214
+ .option('--dry-run', 'Preview what would be copied without making changes')
215
+ .option('--unghost', 'Restore ghosted files from cache')
216
+ .action(handleResync);
217
+ }
218
+ //# sourceMappingURL=resync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resync.js","sourceRoot":"","sources":["../../src/commands/resync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export default function register(program: Command): void;
3
+ //# sourceMappingURL=source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/commands/source.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8EzC,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsBvD"}
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import { getSources, addSource, removeSource, sourceExists } from '../config.js';
3
+ import { validateLocalPath } from '../validate.js';
4
+ import { success, error, dim, printSourceList } from '../ui.js';
5
+ function parseLocation(location) {
6
+ if (location.startsWith('https://')) {
7
+ const url = new URL(location);
8
+ if (url.host !== 'github.com') {
9
+ return null;
10
+ }
11
+ const segments = url.pathname.split('/').filter(Boolean);
12
+ if (segments.length < 2) {
13
+ return null;
14
+ }
15
+ const baseUrl = `${url.protocol}//${url.host}/${segments[0]}/${segments[1]}`;
16
+ const subPath = segments.length > 2 ? '/' + segments.slice(2).join('/') : '';
17
+ return { type: 'git', baseUrl, subPath };
18
+ }
19
+ return { type: 'local' };
20
+ }
21
+ async function handleAdd(name, location) {
22
+ if (sourceExists(name)) {
23
+ error(`source "${name}" already exists`);
24
+ return;
25
+ }
26
+ const parsed = parseLocation(location);
27
+ if (!parsed) {
28
+ error(`invalid location: "${location}"`);
29
+ return;
30
+ }
31
+ if (parsed.type === 'git') {
32
+ addSource({ type: 'git', name, location: parsed.baseUrl, path: parsed.subPath || undefined });
33
+ }
34
+ else {
35
+ const result = validateLocalPath(location);
36
+ if (!result.valid) {
37
+ error(result.error ?? 'invalid local path');
38
+ return;
39
+ }
40
+ addSource({ type: 'local', name, location: path.resolve(location) });
41
+ }
42
+ success(`Source "${name}" added`);
43
+ }
44
+ function handleRemove(name) {
45
+ if (!sourceExists(name)) {
46
+ error(`source "${name}" not found`);
47
+ return;
48
+ }
49
+ removeSource(name);
50
+ success(`Source "${name}" removed`);
51
+ }
52
+ function handleList() {
53
+ const sources = getSources();
54
+ if (sources.length === 0) {
55
+ dim('No sources registered');
56
+ return;
57
+ }
58
+ printSourceList(sources);
59
+ }
60
+ export default function register(program) {
61
+ const source = program
62
+ .command('source')
63
+ .description('Manage asset sources');
64
+ source
65
+ .command('add')
66
+ .description('Add a source (git URL or local path)')
67
+ .argument('<name>', 'Source name')
68
+ .argument('<location>', 'Git URL or local filesystem path')
69
+ .action(handleAdd);
70
+ source
71
+ .command('remove')
72
+ .description('Remove a source by name')
73
+ .argument('<name>', 'Source name')
74
+ .action(handleRemove);
75
+ source
76
+ .command('list')
77
+ .description('List all registered sources')
78
+ .action(handleList);
79
+ }
80
+ //# sourceMappingURL=source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/commands/source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Command } from 'commander';
2
+ interface GitHubFile {
3
+ name: string;
4
+ relativePath: string;
5
+ downloadUrl: string;
6
+ }
7
+ export declare function fetchGitHubFiles(owner: string, repo: string, subPath: string, fileSpec: string | undefined): Promise<GitHubFile[]>;
8
+ export declare function handleSync(sourceSpec: string, destName: string, options: {
9
+ force?: boolean;
10
+ dryRun?: boolean;
11
+ }): Promise<void>;
12
+ export default function register(program: Command): void;
13
+ export {};
14
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0BzC,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAwDxI;AAmDD,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2LpI;AAED,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CASvD"}
@@ -0,0 +1,288 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import checkbox from '@inquirer/checkbox';
5
+ import { getSources, sourceExists, getDestinations, destinationExists, addCopy, getCopiesByDestination, fileCacheDir, fileCachePath, getPref, isTipDismissed, } from '../config.js';
6
+ import { error as uiError, dim } from '../ui.js';
7
+ import { globPattern } from '../glob.js';
8
+ async function selectOverwrites(names) {
9
+ if (names.length === 0)
10
+ return new Set();
11
+ const chosen = await checkbox({
12
+ message: 'Select files to overwrite',
13
+ choices: names.map((n) => ({ name: n, value: n, checked: true })),
14
+ theme: { style: { answer: () => '' } },
15
+ });
16
+ return new Set(chosen);
17
+ }
18
+ export async function fetchGitHubFiles(owner, repo, subPath, fileSpec) {
19
+ const dirPath = subPath ? subPath.replace(/^\//, '') : '';
20
+ let listPath;
21
+ let globPart;
22
+ if (fileSpec !== undefined && fileSpec.includes('*')) {
23
+ // glob: list the directory part, filter by glob
24
+ const lastSlash = fileSpec.lastIndexOf('/');
25
+ const dirPart = lastSlash === -1 ? '' : fileSpec.slice(0, lastSlash);
26
+ globPart = fileSpec.slice(lastSlash + 1);
27
+ listPath = [dirPath, dirPart].filter(Boolean).join('/');
28
+ }
29
+ else if (fileSpec !== undefined) {
30
+ // specific file: listPath is the file itself
31
+ listPath = [dirPath, fileSpec].filter(Boolean).join('/');
32
+ }
33
+ else {
34
+ // all files in subPath dir
35
+ listPath = dirPath;
36
+ }
37
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${listPath}`;
38
+ const res = await fetch(apiUrl, { headers: { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' } });
39
+ if (!res.ok) {
40
+ throw new Error(`GitHub API error ${res.status} for ${apiUrl}`);
41
+ }
42
+ const data = await res.json();
43
+ // Single file response
44
+ if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
45
+ const obj = data;
46
+ if (obj.type === 'file' && typeof obj.name === 'string' && typeof obj.download_url === 'string') {
47
+ const name = fileSpec ?? obj.name;
48
+ return [{ name, relativePath: name, downloadUrl: obj.download_url }];
49
+ }
50
+ // directory object — shouldn't happen for file spec
51
+ return [];
52
+ }
53
+ if (!Array.isArray(data))
54
+ return [];
55
+ let entries = data.filter((e) => e.type === 'file');
56
+ if (globPart !== undefined) {
57
+ const regex = globPattern(globPart);
58
+ entries = entries.filter((e) => typeof e.name === 'string' && regex.test(e.name));
59
+ }
60
+ // Compute the subdir prefix so callers can reconstruct the full source-relative path
61
+ const dirPrefix = globPart !== undefined
62
+ ? (fileSpec !== undefined ? fileSpec.slice(0, fileSpec.lastIndexOf('/') + 1) : '')
63
+ : (fileSpec !== undefined ? `${fileSpec}/` : '');
64
+ return entries
65
+ .filter((e) => typeof e.name === 'string' && typeof e.download_url === 'string')
66
+ .map((e) => ({ name: e.name, relativePath: `${dirPrefix}${e.name}`, downloadUrl: e.download_url }));
67
+ }
68
+ function resolveFiles(workTree, fileSpec) {
69
+ if (fileSpec === undefined) {
70
+ return fs.readdirSync(workTree, { withFileTypes: true })
71
+ .filter((e) => e.isFile())
72
+ .map((e) => ({ src: path.join(workTree, e.name), rel: e.name, sourcePath: e.name }));
73
+ }
74
+ if (fileSpec.includes('*')) {
75
+ const lastSlash = fileSpec.lastIndexOf('/');
76
+ const dirPart = lastSlash === -1 ? '' : fileSpec.slice(0, lastSlash);
77
+ const globPart = fileSpec.slice(lastSlash + 1);
78
+ const regex = globPattern(globPart);
79
+ const dirPath = path.join(workTree, dirPart);
80
+ const resolvedDir = path.resolve(dirPath);
81
+ const baseRes = path.resolve(workTree);
82
+ if (resolvedDir !== baseRes && !resolvedDir.startsWith(baseRes + path.sep)) {
83
+ throw new Error(`fileSpec escapes base directory: ${fileSpec}`);
84
+ }
85
+ if (!fs.existsSync(dirPath)) {
86
+ return [];
87
+ }
88
+ return fs.readdirSync(dirPath, { withFileTypes: true })
89
+ .filter((e) => e.isFile() && regex.test(e.name))
90
+ .map((e) => ({
91
+ src: path.join(dirPath, e.name),
92
+ rel: e.name,
93
+ sourcePath: dirPart ? `${dirPart}/${e.name}` : e.name,
94
+ }));
95
+ }
96
+ // Specific file path
97
+ const srcPath = path.join(workTree, fileSpec);
98
+ const resolvedSrc = path.resolve(srcPath);
99
+ if (!resolvedSrc.startsWith(path.resolve(workTree) + path.sep)) {
100
+ throw new Error(`fileSpec escapes base directory: ${fileSpec}`);
101
+ }
102
+ if (!fs.existsSync(srcPath)) {
103
+ return [];
104
+ }
105
+ if (fs.statSync(srcPath).isDirectory()) {
106
+ return fs.readdirSync(srcPath, { withFileTypes: true })
107
+ .filter((e) => e.isFile())
108
+ .map((e) => ({ src: path.join(srcPath, e.name), rel: e.name, sourcePath: `${fileSpec}/${e.name}` }));
109
+ }
110
+ return [{ src: srcPath, rel: fileSpec, sourcePath: fileSpec }];
111
+ }
112
+ export async function handleSync(sourceSpec, destName, options) {
113
+ const { dryRun } = options;
114
+ const force = (options.force ?? false) || getPref('sync.allowOverwrite');
115
+ // Parse source-spec
116
+ const slashIndex = sourceSpec.indexOf('/');
117
+ let sourceName;
118
+ let fileSpec;
119
+ if (slashIndex === -1) {
120
+ sourceName = sourceSpec;
121
+ fileSpec = undefined;
122
+ }
123
+ else {
124
+ sourceName = sourceSpec.slice(0, slashIndex);
125
+ fileSpec = sourceSpec.slice(slashIndex + 1);
126
+ }
127
+ // Validate
128
+ if (!sourceExists(sourceName)) {
129
+ uiError(`source "${sourceName}" not found`);
130
+ return;
131
+ }
132
+ if (!destinationExists(destName)) {
133
+ uiError(`destination "${destName}" not found`);
134
+ return;
135
+ }
136
+ const source = getSources().find((s) => s.name === sourceName);
137
+ const dest = getDestinations().find((d) => d.name === destName);
138
+ if (source === undefined || dest === undefined) {
139
+ uiError('internal error: source or destination missing after validation');
140
+ return;
141
+ }
142
+ if (!force && !dryRun && !isTipDismissed('sync.allowOverwrite')) {
143
+ console.log(chalk.dim('💡 you can run `scopy config sync.allowOverwrite true` to skip overwrite confirmation'));
144
+ }
145
+ if (source.type === 'git') {
146
+ // Parse owner/repo from stored location (https://github.com/{owner}/{repo})
147
+ const urlParts = new URL(source.location).pathname.split('/').filter(Boolean);
148
+ const owner = urlParts[0];
149
+ const repo = urlParts[1];
150
+ const subPath = source.path ?? '';
151
+ let gitFiles;
152
+ try {
153
+ gitFiles = await fetchGitHubFiles(owner, repo, subPath, fileSpec);
154
+ }
155
+ catch (err) {
156
+ uiError(`error fetching files from GitHub: ${err instanceof Error ? err.message : String(err)}`);
157
+ return;
158
+ }
159
+ if (gitFiles.length === 0) {
160
+ dim('No files to copy');
161
+ return;
162
+ }
163
+ if (dryRun) {
164
+ console.log(`${chalk.dim('Would copy')} ${gitFiles.length} file${gitFiles.length === 1 ? '' : 's'}:`);
165
+ for (const f of gitFiles) {
166
+ console.log(`${chalk.dim('·')} ${f.name}`);
167
+ }
168
+ return;
169
+ }
170
+ let copied = 0;
171
+ let skipped = 0;
172
+ const copyErrors = [];
173
+ const conflicting = gitFiles.filter((f) => fs.existsSync(path.join(dest.location, f.name))).map((f) => f.name);
174
+ const toOverwrite = force ? new Set(conflicting) : await selectOverwrites(conflicting);
175
+ for (const f of gitFiles) {
176
+ const destPath = path.join(dest.location, f.name);
177
+ const existed = fs.existsSync(destPath);
178
+ if (existed && !toOverwrite.has(f.name)) {
179
+ skipped++;
180
+ continue;
181
+ }
182
+ try {
183
+ const contentRes = await fetch(f.downloadUrl);
184
+ if (!contentRes.ok)
185
+ throw new Error(`HTTP ${contentRes.status}`);
186
+ const content = Buffer.from(await contentRes.arrayBuffer());
187
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
188
+ fs.writeFileSync(destPath, content);
189
+ addCopy({ source: sourceName, destination: destName, file: f.name, sourcePath: f.relativePath, copiedAt: new Date().toISOString() });
190
+ const copies = getCopiesByDestination(destName);
191
+ const record = copies.find((c) => c.source === sourceName && c.file === f.name);
192
+ if (record && record.index !== undefined) {
193
+ fs.mkdirSync(fileCacheDir(destName), { recursive: true });
194
+ fs.writeFileSync(fileCachePath(destName, record.index), content);
195
+ }
196
+ console.log(`${chalk.green('✓')} ${f.name}${existed ? chalk.dim(' (overwritten)') : ''}`);
197
+ copied++;
198
+ }
199
+ catch (err) {
200
+ const fileErr = err instanceof Error ? err : new Error(String(err));
201
+ copyErrors.push({ file: f.name, err: fileErr });
202
+ uiError(`${f.name}: ${fileErr.message}`);
203
+ }
204
+ }
205
+ console.log(`${chalk.green(String(copied))} copied, ${chalk.yellow(String(skipped))} skipped`);
206
+ if (copyErrors.length > 0) {
207
+ const noun = copyErrors.length === 1 ? 'error' : 'errors';
208
+ throw new Error(`${copyErrors.length} file ${noun} during sync:\n` +
209
+ copyErrors.map(e => ` ${e.file}: ${e.err.message}`).join('\n'));
210
+ }
211
+ return;
212
+ }
213
+ // Local source
214
+ const workTree = source.location;
215
+ let files;
216
+ try {
217
+ files = resolveFiles(workTree, fileSpec);
218
+ }
219
+ catch (err) {
220
+ uiError(`error reading source files${fileSpec ? ` for "${fileSpec}"` : ''}: ${err instanceof Error ? err.message : String(err)}`);
221
+ return;
222
+ }
223
+ if (files.length === 0) {
224
+ dim('No files to copy');
225
+ return;
226
+ }
227
+ if (dryRun) {
228
+ console.log(`${chalk.dim('Would copy')} ${files.length} file${files.length === 1 ? '' : 's'}:`);
229
+ for (const f of files) {
230
+ console.log(`${chalk.dim('·')} ${f.rel}`);
231
+ }
232
+ return;
233
+ }
234
+ let copied = 0;
235
+ let skipped = 0;
236
+ const copyErrors = [];
237
+ const conflicting = files.filter((f) => fs.existsSync(path.join(dest.location, f.rel))).map((f) => f.rel);
238
+ const toOverwrite = force ? new Set(conflicting) : await selectOverwrites(conflicting);
239
+ for (const f of files) {
240
+ const destPath = path.join(dest.location, f.rel);
241
+ const existed = fs.existsSync(destPath);
242
+ if (existed && !toOverwrite.has(f.rel)) {
243
+ skipped++;
244
+ continue;
245
+ }
246
+ try {
247
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
248
+ fs.copyFileSync(f.src, destPath);
249
+ addCopy({
250
+ source: sourceName,
251
+ destination: destName,
252
+ file: f.rel,
253
+ sourcePath: f.sourcePath,
254
+ copiedAt: new Date().toISOString(),
255
+ });
256
+ const copies = getCopiesByDestination(destName);
257
+ const record = copies.find((c) => c.source === sourceName && c.file === f.rel);
258
+ if (record && record.index !== undefined) {
259
+ fs.mkdirSync(fileCacheDir(destName), { recursive: true });
260
+ fs.copyFileSync(f.src, fileCachePath(destName, record.index));
261
+ }
262
+ console.log(`${chalk.green('✓')} ${f.rel}${existed ? chalk.dim(' (overwritten)') : ''}`);
263
+ copied++;
264
+ }
265
+ catch (err) {
266
+ const error = err instanceof Error ? err : new Error(String(err));
267
+ copyErrors.push({ file: f.rel, err: error });
268
+ uiError(`${f.rel}: ${error.message}`);
269
+ }
270
+ }
271
+ console.log(`${chalk.green(String(copied))} copied, ${chalk.yellow(String(skipped))} skipped`);
272
+ if (copyErrors.length > 0) {
273
+ const noun = copyErrors.length === 1 ? 'error' : 'errors';
274
+ throw new Error(`${copyErrors.length} file ${noun} during sync:\n` +
275
+ copyErrors.map(e => ` ${e.file}: ${e.err.message}`).join('\n'));
276
+ }
277
+ }
278
+ export default function register(program) {
279
+ program
280
+ .command('sync')
281
+ .description('Copy files from a source to a destination')
282
+ .argument('<source-spec>', 'Source name with optional /path/file pattern')
283
+ .argument('<dest>', 'Destination name')
284
+ .option('--force', 'Skip overwrite confirmation')
285
+ .option('--dry-run', 'Preview without copying')
286
+ .action(handleSync);
287
+ }
288
+ //# sourceMappingURL=sync.js.map