@maydotinc/s3-sync 0.1.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.
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # @maydotinc/s3-sync
2
+
3
+ A tool that syncs one or more local directories to S3-compatible storage, designed for GitHub Actions workflows.
4
+
5
+ It performs deterministic diffs (local file hash vs remote object ETag), uploads only changed files, and optionally deletes stale remote files.
6
+
7
+ ---
8
+
9
+ ## Why use this
10
+
11
+ - Sync static assets to S3 (or S3-compatible providers) on every push
12
+ - Support monorepos with one or many sync targets
13
+ - Avoid re-uploading unchanged files
14
+ - Optional Slack/Discord deployment notifications
15
+ - Config is validated with Zod for safer runtime behavior
16
+
17
+ ---
18
+
19
+ ## Requirements
20
+
21
+ - Node.js 22+
22
+ - A GitHub repository with Actions enabled
23
+ - S3 credentials with permissions for listing/uploading (and deleting if enabled)
24
+
25
+ ---
26
+
27
+ ## Quick start
28
+
29
+ ### 1) Configure your first target
30
+
31
+ ```bash
32
+ npx @maydotinc/s3-sync <directory> sync
33
+ ```
34
+
35
+ Example:
36
+
37
+ ```bash
38
+ npx @maydotinc/s3-sync cdn sync --bucket my-assets --region us-east-1 --prefix assets --delete
39
+ ```
40
+
41
+ This creates/updates:
42
+
43
+ - `s3-sync.json`
44
+ - `.github/workflows/s3-sync.yml`
45
+
46
+ ### 2) Add GitHub Actions secrets
47
+
48
+ Required:
49
+
50
+ - `S3_ACCESS_KEY_ID`
51
+ - `S3_SECRET_ACCESS_KEY`
52
+
53
+ Optional (only if notifications are enabled):
54
+
55
+ - `SLACK_WEBHOOK_URL`
56
+ - `DISCORD_WEBHOOK_URL`
57
+
58
+ ### 3) Commit and push
59
+
60
+ Push to your configured branch and the workflow will run automatically.
61
+
62
+ ---
63
+
64
+ ## Command usage
65
+
66
+ ### Setup / add target
67
+
68
+ ```bash
69
+ npx @maydotinc/s3-sync <directory> sync [options]
70
+ ```
71
+
72
+ Options:
73
+
74
+ - `--bucket <name>` S3 bucket name
75
+ - `--region <region>` AWS region
76
+ - `--endpoint <url>` Custom S3-compatible endpoint for this target
77
+ - `--prefix <prefix>` Remote key prefix (folder path in bucket)
78
+ - `--branch <branch>` Branch to trigger workflow
79
+ - `--delete` Delete remote files not present locally
80
+ - `--no-delete` Keep remote-only files
81
+ - `--slack` Enable Slack notifications
82
+ - `--discord` Enable Discord notifications
83
+
84
+ ### Run sync manually
85
+
86
+ ```bash
87
+ npx @maydotinc/s3-sync sync
88
+ ```
89
+
90
+ This is what GitHub Actions runs internally.
91
+
92
+ ---
93
+
94
+ ## Monorepo patterns
95
+
96
+ ### Single consolidated folder
97
+
98
+ If your repo builds everything into one folder (for example `cdn/`), configure one target:
99
+
100
+ ```bash
101
+ npx @maydotinc/s3-sync cdn sync --bucket my-bucket
102
+ ```
103
+
104
+ ### Multiple folders
105
+
106
+ Run setup once per folder:
107
+
108
+ ```bash
109
+ npx @maydotinc/s3-sync apps/web/public sync --bucket my-bucket --prefix web
110
+ npx @maydotinc/s3-sync apps/docs/public sync --bucket my-bucket --prefix docs
111
+ npx @maydotinc/s3-sync apps/admin/dist sync --bucket my-bucket --prefix admin
112
+ ```
113
+
114
+ All targets are stored in a single `s3-sync.json`.
115
+
116
+ ---
117
+
118
+ ## How sync works
119
+
120
+ For each target:
121
+
122
+ 1. Scan local files and compute MD5 hash per file
123
+ 2. List remote objects in the configured bucket + prefix
124
+ 3. Build a diff:
125
+ - upload when key is missing remotely
126
+ - upload when local hash != remote ETag
127
+ - unchanged when local hash == remote ETag
128
+ 4. Optionally delete remote keys missing locally (`delete: true`)
129
+
130
+ No GitHub cache is required for correctness. Every run compares local files against live remote object metadata.
131
+
132
+ ---
133
+
134
+ ## Endpoint behavior (important)
135
+
136
+ Per-target endpoint support is fully supported using `--endpoint` or `target.endpoint` in config.
137
+
138
+ Current resolution behavior:
139
+
140
+ - If a target has its own endpoint, that endpoint is used
141
+ - A global `S3_ENDPOINT` env value is only used when none of the targets define an endpoint
142
+
143
+ This prevents mixed setups (AWS + custom endpoint targets) from accidentally routing all targets to the same endpoint.
144
+
145
+ ---
146
+
147
+ ## Generated configuration
148
+
149
+ `s3-sync.json` structure:
150
+
151
+ ```json
152
+ {
153
+ "targets": [
154
+ {
155
+ "directory": "cdn",
156
+ "bucket": "my-assets",
157
+ "region": "us-east-1",
158
+ "endpoint": "",
159
+ "prefix": "assets",
160
+ "delete": true
161
+ }
162
+ ],
163
+ "branch": "main",
164
+ "notifications": {
165
+ "slack": false,
166
+ "discord": false
167
+ }
168
+ }
169
+ ```
170
+
171
+ Config is validated with Zod on read/write. Invalid config shape fails fast with useful field-level error output.
172
+
173
+ ---
174
+
175
+ ## Workflow details
176
+
177
+ Generated file: `.github/workflows/s3-sync.yml`
178
+
179
+ Key behavior:
180
+
181
+ - Triggered on your configured branch
182
+ - Triggered only when files under configured target directories change
183
+ - Runs `npx --yes <package>@<version> sync`
184
+
185
+ ---
186
+
187
+ ## FAQs
188
+
189
+ ### Does it upload unchanged files every run?
190
+
191
+ No. Unchanged files are skipped based on hash/ETag comparison.
192
+
193
+ ### If I update `assets/logo.png`, will it upload the new version?
194
+
195
+ Yes. Same key + changed content = new upload (overwrite).
196
+
197
+ ### Does it scan my whole bucket?
198
+
199
+ No, unless your prefix is empty. It lists objects scoped to each target's configured prefix.
200
+
201
+ ### Can I use `assets/` instead of `public/`?
202
+
203
+ Yes. Any directory path is supported.
204
+
205
+ ---
206
+
207
+ ## Troubleshooting
208
+
209
+ ### `Missing S3_ACCESS_KEY_ID or S3_SECRET_ACCESS_KEY`
210
+
211
+ Add required secrets to GitHub repository settings:
212
+ `Settings -> Secrets and variables -> Actions`.
213
+
214
+ ### `Directory "<name>" does not exist`
215
+
216
+ Ensure the target directory exists at workflow runtime (after build steps, if applicable).
217
+
218
+ ### Sync runs but uploads unexpectedly many files
219
+
220
+ Check:
221
+
222
+ - prefix configuration changed
223
+ - files were rebuilt with different content hashes
224
+ - remote objects were modified outside this tool
225
+
226
+ ### Invalid config error
227
+
228
+ Open `s3-sync.json` and fix fields listed in the validation error message.
229
+
230
+ ## Security notes
231
+
232
+ - Use least-privilege IAM credentials for your target bucket/prefix.
233
+ - If delete is enabled, credentials must allow delete operations.
234
+ - Store credentials only in GitHub secrets.
package/dist/index.js ADDED
@@ -0,0 +1,726 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/setup.ts
7
+ import { mkdir, writeFile as writeFile2 } from "fs/promises";
8
+ import { existsSync as existsSync3 } from "fs";
9
+ import path3 from "path";
10
+ import inquirer from "inquirer";
11
+
12
+ // src/utils/config.ts
13
+ import { readFile, writeFile } from "fs/promises";
14
+ import { existsSync } from "fs";
15
+ import path from "path";
16
+ import { z } from "zod";
17
+ var CONFIG_FILE = "s3-sync.json";
18
+ var syncTargetSchema = z.object({
19
+ directory: z.string().min(1),
20
+ bucket: z.string().min(1),
21
+ region: z.string().min(1),
22
+ endpoint: z.string().default(""),
23
+ prefix: z.string().default(""),
24
+ delete: z.boolean().default(false)
25
+ });
26
+ var notificationsSchema = z.object({
27
+ slack: z.boolean().default(false),
28
+ discord: z.boolean().default(false)
29
+ });
30
+ var configSchema = z.object({
31
+ targets: z.array(syncTargetSchema).default([]),
32
+ branch: z.string().min(1).default("main"),
33
+ notifications: notificationsSchema.default({ slack: false, discord: false })
34
+ });
35
+ var legacyConfigSchema = z.object({
36
+ directory: z.string().min(1),
37
+ bucket: z.string().min(1),
38
+ region: z.string().min(1),
39
+ endpoint: z.string().default(""),
40
+ prefix: z.string().default(""),
41
+ branch: z.string().min(1).default("main"),
42
+ delete: z.boolean().default(false),
43
+ notifications: notificationsSchema.default({ slack: false, discord: false })
44
+ });
45
+ function getConfigPath(cwd = process.cwd()) {
46
+ return path.join(cwd, CONFIG_FILE);
47
+ }
48
+ function configExists(cwd = process.cwd()) {
49
+ return existsSync(getConfigPath(cwd));
50
+ }
51
+ async function readConfig(cwd = process.cwd()) {
52
+ const configPath = getConfigPath(cwd);
53
+ const raw = JSON.parse(await readFile(configPath, "utf-8"));
54
+ const parsedConfig = configSchema.safeParse(raw);
55
+ if (parsedConfig.success) return parsedConfig.data;
56
+ const parsedLegacyConfig = legacyConfigSchema.safeParse(raw);
57
+ if (!parsedLegacyConfig.success) {
58
+ const issues = parsedConfig.error.issues.map((issue) => {
59
+ const path6 = issue.path.length > 0 ? issue.path.join(".") : "root";
60
+ return `${path6}: ${issue.message}`;
61
+ }).join("; ");
62
+ throw new Error(`Invalid ${CONFIG_FILE}: ${issues}`);
63
+ }
64
+ const legacy = parsedLegacyConfig.data;
65
+ return {
66
+ targets: [
67
+ {
68
+ directory: legacy.directory,
69
+ bucket: legacy.bucket,
70
+ region: legacy.region,
71
+ endpoint: legacy.endpoint,
72
+ prefix: legacy.prefix,
73
+ delete: legacy.delete
74
+ }
75
+ ],
76
+ branch: legacy.branch,
77
+ notifications: legacy.notifications
78
+ };
79
+ }
80
+ async function writeConfig(config, cwd = process.cwd()) {
81
+ const configPath = getConfigPath(cwd);
82
+ const validatedConfig = configSchema.parse(config);
83
+ await writeFile(
84
+ configPath,
85
+ JSON.stringify(validatedConfig, null, 2) + "\n",
86
+ "utf-8"
87
+ );
88
+ }
89
+ function defaultTarget() {
90
+ return {
91
+ directory: "public",
92
+ bucket: "",
93
+ region: "us-east-1",
94
+ endpoint: "",
95
+ prefix: "",
96
+ delete: false
97
+ };
98
+ }
99
+ function defaultConfig() {
100
+ return {
101
+ targets: [],
102
+ branch: "main",
103
+ notifications: { slack: false, discord: false }
104
+ };
105
+ }
106
+
107
+ // src/workflow/generator.ts
108
+ function generateWorkflow(config, packageSpecifier) {
109
+ const envBlock = buildEnvBlock(config);
110
+ const pathFilters = config.targets.map((t) => ` - '${t.directory}/**'`).join("\n");
111
+ return `name: S3 Sync
112
+
113
+ on:
114
+ push:
115
+ branches: [${config.branch}]
116
+ paths:
117
+ ${pathFilters}
118
+
119
+ jobs:
120
+ sync:
121
+ name: Sync to S3
122
+ runs-on: ubuntu-latest
123
+
124
+ steps:
125
+ - name: Checkout
126
+ uses: actions/checkout@v4
127
+
128
+ - name: Setup Node
129
+ uses: actions/setup-node@v4
130
+ with:
131
+ node-version: "22"
132
+
133
+ - name: Sync files
134
+ run: npx --yes ${packageSpecifier} sync
135
+ ${envBlock}
136
+ `;
137
+ }
138
+ function buildEnvBlock(config) {
139
+ const lines = [
140
+ " env:",
141
+ " S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}",
142
+ " S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}"
143
+ ];
144
+ if (config.notifications.slack) {
145
+ lines.push(
146
+ " SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}"
147
+ );
148
+ }
149
+ if (config.notifications.discord) {
150
+ lines.push(
151
+ " DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}"
152
+ );
153
+ }
154
+ return lines.join("\n");
155
+ }
156
+
157
+ // src/utils/logger.ts
158
+ import chalk from "chalk";
159
+ var log = {
160
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
161
+ success: (msg) => console.log(chalk.green("\u2713"), msg),
162
+ warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
163
+ error: (msg) => console.error(chalk.red("\u2717"), msg),
164
+ dim: (msg) => console.log(chalk.dim(msg)),
165
+ heading: (msg) => console.log(chalk.bold.cyan(`
166
+ ${msg}
167
+ `))
168
+ };
169
+
170
+ // src/utils/package-meta.ts
171
+ import { existsSync as existsSync2, readFileSync } from "fs";
172
+ import path2 from "path";
173
+ import { fileURLToPath } from "url";
174
+ function findPackageJson(startDirectory) {
175
+ let currentDirectory = startDirectory;
176
+ while (true) {
177
+ const candidate = path2.join(currentDirectory, "package.json");
178
+ if (existsSync2(candidate)) return candidate;
179
+ const parentDirectory = path2.dirname(currentDirectory);
180
+ if (parentDirectory === currentDirectory) return void 0;
181
+ currentDirectory = parentDirectory;
182
+ }
183
+ }
184
+ function getCurrentPackageSpecifier(fallbackName = "@maydotinc/s3-sync") {
185
+ const currentFileDirectory = path2.dirname(fileURLToPath(import.meta.url));
186
+ const packageJsonPath = findPackageJson(currentFileDirectory);
187
+ if (!packageJsonPath) return fallbackName;
188
+ try {
189
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
190
+ const packageName = parsed.name?.trim() || fallbackName;
191
+ const packageVersion = parsed.version?.trim();
192
+ return packageVersion ? `${packageName}@${packageVersion}` : packageName;
193
+ } catch {
194
+ return fallbackName;
195
+ }
196
+ }
197
+
198
+ // src/commands/setup.ts
199
+ async function setupCommand(directory, flags) {
200
+ log.heading("s3-sync setup");
201
+ let config;
202
+ let isAdding = false;
203
+ if (configExists()) {
204
+ const existing = await readConfig();
205
+ const alreadyHasDir = existing.targets.some(
206
+ (t) => t.directory === directory
207
+ );
208
+ if (alreadyHasDir) {
209
+ const { overwrite } = await inquirer.prompt([
210
+ {
211
+ type: "confirm",
212
+ name: "overwrite",
213
+ message: `Target "${directory}" already exists. Overwrite it?`,
214
+ default: false
215
+ }
216
+ ]);
217
+ if (!overwrite) {
218
+ log.info("Setup cancelled.");
219
+ return;
220
+ }
221
+ existing.targets = existing.targets.filter(
222
+ (t) => t.directory !== directory
223
+ );
224
+ } else {
225
+ log.info(
226
+ `Existing config found with ${existing.targets.length} target(s). Adding "${directory}".`
227
+ );
228
+ isAdding = true;
229
+ }
230
+ config = existing;
231
+ } else {
232
+ config = defaultConfig();
233
+ }
234
+ const dirPath = path3.resolve(directory);
235
+ if (!existsSync3(dirPath)) {
236
+ log.warn(
237
+ `Directory "${directory}" doesn't exist yet \u2014 that's okay, it will be created later.`
238
+ );
239
+ }
240
+ const target = await gatherTarget(directory, flags);
241
+ config.targets.push(target);
242
+ if (!isAdding) {
243
+ await gatherSharedConfig(config, flags);
244
+ }
245
+ await writeConfig(config);
246
+ log.success("Updated s3-sync.json");
247
+ const workflowDir = path3.join(process.cwd(), ".github", "workflows");
248
+ await mkdir(workflowDir, { recursive: true });
249
+ const workflowPath = path3.join(workflowDir, "s3-sync.yml");
250
+ const packageSpecifier = getCurrentPackageSpecifier();
251
+ const workflowContent = generateWorkflow(config, packageSpecifier);
252
+ await writeFile2(workflowPath, workflowContent, "utf-8");
253
+ log.success("Updated .github/workflows/s3-sync.yml");
254
+ printSecretInstructions(config);
255
+ }
256
+ async function gatherTarget(directory, flags) {
257
+ const target = defaultTarget();
258
+ target.directory = directory;
259
+ const questions = [];
260
+ if (flags.bucket === void 0) {
261
+ questions.push({
262
+ type: "input",
263
+ name: "bucket",
264
+ message: `[${directory}] S3 bucket name:`,
265
+ validate: (v) => v.trim() ? true : "Bucket name is required"
266
+ });
267
+ } else {
268
+ target.bucket = flags.bucket;
269
+ }
270
+ if (flags.region === void 0) {
271
+ questions.push({
272
+ type: "input",
273
+ name: "region",
274
+ message: `[${directory}] AWS region:`,
275
+ default: "us-east-1"
276
+ });
277
+ } else {
278
+ target.region = flags.region;
279
+ }
280
+ if (flags.endpoint === void 0) {
281
+ questions.push({
282
+ type: "input",
283
+ name: "endpoint",
284
+ message: `[${directory}] Custom S3 endpoint (leave blank for AWS):`,
285
+ default: ""
286
+ });
287
+ } else {
288
+ target.endpoint = flags.endpoint;
289
+ }
290
+ if (flags.prefix === void 0) {
291
+ questions.push({
292
+ type: "input",
293
+ name: "prefix",
294
+ message: `[${directory}] Path prefix (leave blank for root):`,
295
+ default: ""
296
+ });
297
+ } else {
298
+ target.prefix = flags.prefix;
299
+ }
300
+ if (flags.delete === void 0) {
301
+ questions.push({
302
+ type: "confirm",
303
+ name: "delete",
304
+ message: `[${directory}] Delete files from S3 that no longer exist locally?`,
305
+ default: false
306
+ });
307
+ } else {
308
+ target.delete = flags.delete;
309
+ }
310
+ if (questions.length > 0) {
311
+ const answers = await inquirer.prompt(questions);
312
+ if (answers.bucket !== void 0) target.bucket = answers.bucket;
313
+ if (answers.region !== void 0) target.region = answers.region;
314
+ if (answers.endpoint !== void 0) target.endpoint = answers.endpoint;
315
+ if (answers.prefix !== void 0) target.prefix = answers.prefix;
316
+ if (answers.delete !== void 0) target.delete = answers.delete;
317
+ }
318
+ return target;
319
+ }
320
+ async function gatherSharedConfig(config, flags) {
321
+ const questions = [];
322
+ if (flags.branch === void 0) {
323
+ questions.push({
324
+ type: "input",
325
+ name: "branch",
326
+ message: "Branch to trigger sync on:",
327
+ default: "main"
328
+ });
329
+ } else {
330
+ config.branch = flags.branch;
331
+ }
332
+ if (flags.slack === void 0) {
333
+ questions.push({
334
+ type: "confirm",
335
+ name: "slack",
336
+ message: "Enable Slack notifications?",
337
+ default: false
338
+ });
339
+ } else {
340
+ config.notifications.slack = flags.slack;
341
+ }
342
+ if (flags.discord === void 0) {
343
+ questions.push({
344
+ type: "confirm",
345
+ name: "discord",
346
+ message: "Enable Discord notifications?",
347
+ default: false
348
+ });
349
+ } else {
350
+ config.notifications.discord = flags.discord;
351
+ }
352
+ if (questions.length > 0) {
353
+ const answers = await inquirer.prompt(questions);
354
+ if (answers.branch !== void 0) config.branch = answers.branch;
355
+ if (answers.slack !== void 0) config.notifications.slack = answers.slack;
356
+ if (answers.discord !== void 0)
357
+ config.notifications.discord = answers.discord;
358
+ }
359
+ }
360
+ function printSecretInstructions(config) {
361
+ log.heading("Next steps");
362
+ log.info("Add these secrets to your GitHub repository:");
363
+ log.dim(
364
+ " Settings \u2192 Secrets and variables \u2192 Actions \u2192 New repository secret\n"
365
+ );
366
+ const secrets = [
367
+ ["S3_ACCESS_KEY_ID", "Your S3 access key"],
368
+ ["S3_SECRET_ACCESS_KEY", "Your S3 secret key"]
369
+ ];
370
+ if (config.notifications.slack) {
371
+ secrets.push(["SLACK_WEBHOOK_URL", "Slack incoming webhook URL"]);
372
+ }
373
+ if (config.notifications.discord) {
374
+ secrets.push(["DISCORD_WEBHOOK_URL", "Discord webhook URL"]);
375
+ }
376
+ for (const [name, desc] of secrets) {
377
+ log.dim(` \u2022 ${name} \u2014 ${desc}`);
378
+ }
379
+ console.log();
380
+ log.heading("Configured targets");
381
+ for (const t of config.targets) {
382
+ log.info(` ${t.directory}/ \u2192 s3://${t.bucket}/${t.prefix}`);
383
+ }
384
+ console.log();
385
+ log.success(
386
+ `Push to "${config.branch}" to trigger a sync.`
387
+ );
388
+ }
389
+
390
+ // src/commands/sync.ts
391
+ import { existsSync as existsSync4 } from "fs";
392
+ import path5 from "path";
393
+
394
+ // src/core/fingerprint.ts
395
+ import { createHash } from "crypto";
396
+ import { readFile as readFile2 } from "fs/promises";
397
+ import path4 from "path";
398
+ import { glob } from "glob";
399
+ async function fingerprintDirectory(directory) {
400
+ const absoluteDir = path4.resolve(directory);
401
+ const files = await glob("**/*", {
402
+ cwd: absoluteDir,
403
+ nodir: true,
404
+ dot: false
405
+ });
406
+ const results = [];
407
+ await Promise.all(
408
+ files.map(async (relativePath) => {
409
+ const absolutePath = path4.join(absoluteDir, relativePath);
410
+ const content = await readFile2(absolutePath);
411
+ const md5 = createHash("md5").update(content).digest("hex");
412
+ results.push({
413
+ relativePath: relativePath.replace(/\\/g, "/"),
414
+ absolutePath,
415
+ md5,
416
+ size: content.length
417
+ });
418
+ })
419
+ );
420
+ return results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
421
+ }
422
+
423
+ // src/core/s3-client.ts
424
+ import {
425
+ S3Client,
426
+ ListObjectsV2Command,
427
+ PutObjectCommand,
428
+ DeleteObjectCommand
429
+ } from "@aws-sdk/client-s3";
430
+ import { readFile as readFile3 } from "fs/promises";
431
+ import mime from "mime-types";
432
+ function createS3(config) {
433
+ return new S3Client({
434
+ region: config.region,
435
+ credentials: {
436
+ accessKeyId: config.accessKeyId,
437
+ secretAccessKey: config.secretAccessKey
438
+ },
439
+ ...config.endpoint && {
440
+ endpoint: config.endpoint,
441
+ forcePathStyle: true
442
+ }
443
+ });
444
+ }
445
+ async function listAllObjects(client, bucket, prefix) {
446
+ const objects = [];
447
+ let continuationToken;
448
+ do {
449
+ const command = new ListObjectsV2Command({
450
+ Bucket: bucket,
451
+ Prefix: prefix || void 0,
452
+ ContinuationToken: continuationToken
453
+ });
454
+ const response = await client.send(command);
455
+ if (response.Contents) {
456
+ for (const obj of response.Contents) {
457
+ if (obj.Key && obj.ETag && obj.Size !== void 0) {
458
+ objects.push({
459
+ key: obj.Key,
460
+ etag: obj.ETag.replace(/"/g, ""),
461
+ size: obj.Size
462
+ });
463
+ }
464
+ }
465
+ }
466
+ continuationToken = response.IsTruncated ? response.NextContinuationToken : void 0;
467
+ } while (continuationToken);
468
+ return objects;
469
+ }
470
+ async function uploadFile(client, bucket, key, filePath) {
471
+ const body = await readFile3(filePath);
472
+ const contentType = mime.lookup(filePath) || "application/octet-stream";
473
+ await client.send(
474
+ new PutObjectCommand({
475
+ Bucket: bucket,
476
+ Key: key,
477
+ Body: body,
478
+ ContentType: contentType
479
+ })
480
+ );
481
+ }
482
+ async function deleteObject(client, bucket, key) {
483
+ await client.send(
484
+ new DeleteObjectCommand({
485
+ Bucket: bucket,
486
+ Key: key
487
+ })
488
+ );
489
+ }
490
+
491
+ // src/core/diff.ts
492
+ function computeDiff(localFiles, remoteObjects, prefix, shouldDelete) {
493
+ const remoteByKey = /* @__PURE__ */ new Map();
494
+ for (const obj of remoteObjects) {
495
+ remoteByKey.set(obj.key, obj);
496
+ }
497
+ const toUpload = [];
498
+ const unchanged = [];
499
+ const localKeys = /* @__PURE__ */ new Set();
500
+ for (const file of localFiles) {
501
+ const key = prefix ? `${prefix}/${file.relativePath}` : file.relativePath;
502
+ localKeys.add(key);
503
+ const remote = remoteByKey.get(key);
504
+ if (!remote || remote.etag !== file.md5) {
505
+ toUpload.push(file);
506
+ } else {
507
+ unchanged.push(file);
508
+ }
509
+ }
510
+ const toDelete = [];
511
+ if (shouldDelete) {
512
+ for (const obj of remoteObjects) {
513
+ if (!localKeys.has(obj.key)) {
514
+ toDelete.push(obj);
515
+ }
516
+ }
517
+ }
518
+ return { toUpload, toDelete, unchanged };
519
+ }
520
+
521
+ // src/notifications/slack.ts
522
+ async function sendSlackNotification(webhookUrl, message) {
523
+ const response = await fetch(webhookUrl, {
524
+ method: "POST",
525
+ headers: { "Content-Type": "application/json" },
526
+ body: JSON.stringify({ text: message })
527
+ });
528
+ if (!response.ok) {
529
+ throw new Error(`Slack notification failed: ${response.statusText}`);
530
+ }
531
+ }
532
+
533
+ // src/notifications/discord.ts
534
+ async function sendDiscordNotification(webhookUrl, message) {
535
+ const response = await fetch(webhookUrl, {
536
+ method: "POST",
537
+ headers: { "Content-Type": "application/json" },
538
+ body: JSON.stringify({ content: message })
539
+ });
540
+ if (!response.ok) {
541
+ throw new Error(`Discord notification failed: ${response.statusText}`);
542
+ }
543
+ }
544
+
545
+ // src/commands/sync.ts
546
+ async function syncCommand() {
547
+ log.heading("s3-sync");
548
+ const config = await readConfig();
549
+ const accessKeyId = process.env.S3_ACCESS_KEY_ID;
550
+ const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY;
551
+ if (!accessKeyId || !secretAccessKey) {
552
+ log.error(
553
+ "Missing S3_ACCESS_KEY_ID or S3_SECRET_ACCESS_KEY environment variables."
554
+ );
555
+ process.exit(1);
556
+ }
557
+ if (config.targets.length === 0) {
558
+ log.error("No sync targets configured. Run setup first.");
559
+ process.exit(1);
560
+ }
561
+ const summaries = [];
562
+ let hasErrors = false;
563
+ const globalEndpoint = process.env.S3_ENDPOINT?.trim() || void 0;
564
+ const hasTargetSpecificEndpoints = config.targets.some(
565
+ (target) => target.endpoint.trim().length > 0
566
+ );
567
+ for (const target of config.targets) {
568
+ try {
569
+ const endpoint = resolveEndpoint(
570
+ target.endpoint,
571
+ globalEndpoint,
572
+ hasTargetSpecificEndpoints
573
+ );
574
+ const diff = await syncTarget(
575
+ target,
576
+ accessKeyId,
577
+ secretAccessKey,
578
+ endpoint
579
+ );
580
+ summaries.push(buildTargetSummary(target, diff));
581
+ } catch (err) {
582
+ log.error(`[${target.directory}] ${err.message}`);
583
+ hasErrors = true;
584
+ }
585
+ }
586
+ if (summaries.length > 0) {
587
+ const fullSummary = summaries.join("\n\n---\n\n");
588
+ await notify(config, fullSummary);
589
+ }
590
+ if (hasErrors) {
591
+ process.exit(1);
592
+ }
593
+ log.success("All targets synced.");
594
+ }
595
+ async function syncTarget(target, accessKeyId, secretAccessKey, endpoint) {
596
+ log.heading(`Syncing ${target.directory}/ \u2192 s3://${target.bucket}/${target.prefix}`);
597
+ const directory = path5.resolve(target.directory);
598
+ if (!existsSync4(directory)) {
599
+ throw new Error(`Directory "${target.directory}" does not exist.`);
600
+ }
601
+ const client = createS3({
602
+ bucket: target.bucket,
603
+ region: target.region,
604
+ endpoint,
605
+ accessKeyId,
606
+ secretAccessKey
607
+ });
608
+ log.info(`Scanning ${target.directory}/...`);
609
+ const localFiles = await fingerprintDirectory(directory);
610
+ log.info(`Found ${localFiles.length} local files`);
611
+ log.info(`Listing objects in s3://${target.bucket}/${target.prefix}...`);
612
+ const remoteObjects = await listAllObjects(
613
+ client,
614
+ target.bucket,
615
+ target.prefix
616
+ );
617
+ log.info(`Found ${remoteObjects.length} remote objects`);
618
+ const diff = computeDiff(
619
+ localFiles,
620
+ remoteObjects,
621
+ target.prefix,
622
+ target.delete
623
+ );
624
+ log.info(
625
+ `${diff.toUpload.length} to upload, ${diff.toDelete.length} to delete, ${diff.unchanged.length} unchanged`
626
+ );
627
+ if (diff.toUpload.length === 0 && diff.toDelete.length === 0) {
628
+ log.success(`[${target.directory}] Already in sync.`);
629
+ return diff;
630
+ }
631
+ for (const file of diff.toUpload) {
632
+ const key = target.prefix ? `${target.prefix}/${file.relativePath}` : file.relativePath;
633
+ log.dim(` uploading ${key}`);
634
+ await uploadFile(client, target.bucket, key, file.absolutePath);
635
+ }
636
+ if (diff.toUpload.length > 0) {
637
+ log.success(`[${target.directory}] Uploaded ${diff.toUpload.length} files`);
638
+ }
639
+ for (const obj of diff.toDelete) {
640
+ log.dim(` deleting ${obj.key}`);
641
+ await deleteObject(client, target.bucket, obj.key);
642
+ }
643
+ if (diff.toDelete.length > 0) {
644
+ log.success(`[${target.directory}] Deleted ${diff.toDelete.length} files`);
645
+ }
646
+ return diff;
647
+ }
648
+ function resolveEndpoint(targetEndpoint, globalEndpoint, hasTargetSpecificEndpoints) {
649
+ const trimmedTargetEndpoint = targetEndpoint.trim();
650
+ if (trimmedTargetEndpoint) return trimmedTargetEndpoint;
651
+ if (!hasTargetSpecificEndpoints) return globalEndpoint;
652
+ return void 0;
653
+ }
654
+ function buildTargetSummary(target, diff) {
655
+ const lines = [
656
+ `*${target.directory}/ \u2192 s3://${target.bucket}/${target.prefix}*`,
657
+ `Uploaded: ${diff.toUpload.length}`,
658
+ `Deleted: ${diff.toDelete.length}`,
659
+ `Unchanged: ${diff.unchanged.length}`
660
+ ];
661
+ if (diff.toUpload.length > 0) {
662
+ const fileList = diff.toUpload.slice(0, 10).map((f) => ` \u2022 ${f.relativePath}`).join("\n");
663
+ lines.push(`
664
+ Uploaded files:
665
+ ${fileList}`);
666
+ if (diff.toUpload.length > 10) {
667
+ lines.push(` ...and ${diff.toUpload.length - 10} more`);
668
+ }
669
+ }
670
+ return lines.join("\n");
671
+ }
672
+ async function notify(config, message) {
673
+ if (config.notifications?.slack) {
674
+ const webhookUrl = process.env.SLACK_WEBHOOK_URL;
675
+ if (webhookUrl) {
676
+ try {
677
+ await sendSlackNotification(webhookUrl, message);
678
+ log.success("Slack notification sent");
679
+ } catch (err) {
680
+ log.warn(`Slack notification failed: ${err.message}`);
681
+ }
682
+ }
683
+ }
684
+ if (config.notifications?.discord) {
685
+ const webhookUrl = process.env.DISCORD_WEBHOOK_URL;
686
+ if (webhookUrl) {
687
+ try {
688
+ await sendDiscordNotification(webhookUrl, message);
689
+ log.success("Discord notification sent");
690
+ } catch (err) {
691
+ log.warn(`Discord notification failed: ${err.message}`);
692
+ }
693
+ }
694
+ }
695
+ }
696
+
697
+ // src/index.ts
698
+ var program = new Command();
699
+ program.name("s3-sync").description("Sync local directories to S3-compatible buckets via GitHub Actions").version("0.1.0");
700
+ program.command("sync").description("Execute sync for all configured targets").action(async () => {
701
+ try {
702
+ await syncCommand();
703
+ } catch (err) {
704
+ console.error(err.message);
705
+ process.exit(1);
706
+ }
707
+ });
708
+ program.argument("<directory>", "Local directory to sync (e.g. public, apps/web/dist)").argument("sync", "Generate workflow and config for syncing this directory").option("--bucket <name>", "S3 bucket name").option("--region <region>", "AWS region").option("--endpoint <url>", "Custom S3-compatible endpoint").option("--prefix <prefix>", "Path prefix in bucket").option("--branch <branch>", "Git branch to trigger on").option("--delete", "Delete remote files not present locally").option("--no-delete", "Keep remote files not present locally").option("--slack", "Enable Slack notifications").option("--discord", "Enable Discord notifications").action(async (directory, _syncArg, options) => {
709
+ try {
710
+ await setupCommand(directory, {
711
+ bucket: options.bucket,
712
+ region: options.region,
713
+ endpoint: options.endpoint,
714
+ prefix: options.prefix,
715
+ branch: options.branch,
716
+ delete: options.delete === true ? true : options.delete === false ? false : void 0,
717
+ slack: options.slack,
718
+ discord: options.discord
719
+ });
720
+ } catch (err) {
721
+ console.error(err.message);
722
+ process.exit(1);
723
+ }
724
+ });
725
+ program.parse();
726
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/commands/setup.ts","../src/utils/config.ts","../src/workflow/generator.ts","../src/utils/logger.ts","../src/utils/package-meta.ts","../src/commands/sync.ts","../src/core/fingerprint.ts","../src/core/s3-client.ts","../src/core/diff.ts","../src/notifications/slack.ts","../src/notifications/discord.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { setupCommand } from \"./commands/setup.js\";\nimport { syncCommand } from \"./commands/sync.js\";\n\nconst program = new Command();\n\nprogram\n .name(\"s3-sync\")\n .description(\"Sync local directories to S3-compatible buckets via GitHub Actions\")\n .version(\"0.1.0\");\n\nprogram\n .command(\"sync\")\n .description(\"Execute sync for all configured targets\")\n .action(async () => {\n try {\n await syncCommand();\n } catch (err: any) {\n console.error(err.message);\n process.exit(1);\n }\n });\n\nprogram\n .argument(\"<directory>\", \"Local directory to sync (e.g. public, apps/web/dist)\")\n .argument(\"sync\", \"Generate workflow and config for syncing this directory\")\n .option(\"--bucket <name>\", \"S3 bucket name\")\n .option(\"--region <region>\", \"AWS region\")\n .option(\"--endpoint <url>\", \"Custom S3-compatible endpoint\")\n .option(\"--prefix <prefix>\", \"Path prefix in bucket\")\n .option(\"--branch <branch>\", \"Git branch to trigger on\")\n .option(\"--delete\", \"Delete remote files not present locally\")\n .option(\"--no-delete\", \"Keep remote files not present locally\")\n .option(\"--slack\", \"Enable Slack notifications\")\n .option(\"--discord\", \"Enable Discord notifications\")\n .action(async (directory: string, _syncArg: string, options: any) => {\n try {\n await setupCommand(directory, {\n bucket: options.bucket,\n region: options.region,\n endpoint: options.endpoint,\n prefix: options.prefix,\n branch: options.branch,\n delete: options.delete === true ? true : options.delete === false ? false : undefined,\n slack: options.slack,\n discord: options.discord,\n });\n } catch (err: any) {\n console.error(err.message);\n process.exit(1);\n }\n });\n\nprogram.parse();\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport inquirer from \"inquirer\";\nimport {\n defaultConfig,\n defaultTarget,\n writeConfig,\n readConfig,\n configExists,\n type S3SyncConfig,\n type SyncTarget,\n} from \"../utils/config.js\";\nimport { generateWorkflow } from \"../workflow/generator.js\";\nimport { log } from \"../utils/logger.js\";\nimport { getCurrentPackageSpecifier } from \"../utils/package-meta.js\";\n\ninterface SetupFlags {\n bucket?: string;\n region?: string;\n endpoint?: string;\n prefix?: string;\n branch?: string;\n delete?: boolean;\n slack?: boolean;\n discord?: boolean;\n}\n\nexport async function setupCommand(\n directory: string,\n flags: SetupFlags\n): Promise<void> {\n log.heading(\"s3-sync setup\");\n\n let config: S3SyncConfig;\n let isAdding = false;\n\n if (configExists()) {\n const existing = await readConfig();\n const alreadyHasDir = existing.targets.some(\n (t) => t.directory === directory\n );\n\n if (alreadyHasDir) {\n const { overwrite } = await inquirer.prompt([\n {\n type: \"confirm\",\n name: \"overwrite\",\n message: `Target \"${directory}\" already exists. Overwrite it?`,\n default: false,\n },\n ]);\n if (!overwrite) {\n log.info(\"Setup cancelled.\");\n return;\n }\n existing.targets = existing.targets.filter(\n (t) => t.directory !== directory\n );\n } else {\n log.info(\n `Existing config found with ${existing.targets.length} target(s). Adding \"${directory}\".`\n );\n isAdding = true;\n }\n config = existing;\n } else {\n config = defaultConfig();\n }\n\n const dirPath = path.resolve(directory);\n if (!existsSync(dirPath)) {\n log.warn(\n `Directory \"${directory}\" doesn't exist yet — that's okay, it will be created later.`\n );\n }\n\n const target = await gatherTarget(directory, flags);\n config.targets.push(target);\n\n if (!isAdding) {\n await gatherSharedConfig(config, flags);\n }\n\n await writeConfig(config);\n log.success(\"Updated s3-sync.json\");\n\n const workflowDir = path.join(process.cwd(), \".github\", \"workflows\");\n await mkdir(workflowDir, { recursive: true });\n\n const workflowPath = path.join(workflowDir, \"s3-sync.yml\");\n const packageSpecifier = getCurrentPackageSpecifier();\n const workflowContent = generateWorkflow(config, packageSpecifier);\n await writeFile(workflowPath, workflowContent, \"utf-8\");\n log.success(\"Updated .github/workflows/s3-sync.yml\");\n\n printSecretInstructions(config);\n}\n\nasync function gatherTarget(\n directory: string,\n flags: SetupFlags\n): Promise<SyncTarget> {\n const target = defaultTarget();\n target.directory = directory;\n\n const questions: any[] = [];\n\n if (flags.bucket === undefined) {\n questions.push({\n type: \"input\",\n name: \"bucket\",\n message: `[${directory}] S3 bucket name:`,\n validate: (v: string) => (v.trim() ? true : \"Bucket name is required\"),\n });\n } else {\n target.bucket = flags.bucket;\n }\n\n if (flags.region === undefined) {\n questions.push({\n type: \"input\",\n name: \"region\",\n message: `[${directory}] AWS region:`,\n default: \"us-east-1\",\n });\n } else {\n target.region = flags.region;\n }\n\n if (flags.endpoint === undefined) {\n questions.push({\n type: \"input\",\n name: \"endpoint\",\n message: `[${directory}] Custom S3 endpoint (leave blank for AWS):`,\n default: \"\",\n });\n } else {\n target.endpoint = flags.endpoint;\n }\n\n if (flags.prefix === undefined) {\n questions.push({\n type: \"input\",\n name: \"prefix\",\n message: `[${directory}] Path prefix (leave blank for root):`,\n default: \"\",\n });\n } else {\n target.prefix = flags.prefix;\n }\n\n if (flags.delete === undefined) {\n questions.push({\n type: \"confirm\",\n name: \"delete\",\n message: `[${directory}] Delete files from S3 that no longer exist locally?`,\n default: false,\n });\n } else {\n target.delete = flags.delete;\n }\n\n if (questions.length > 0) {\n const answers = await inquirer.prompt(questions);\n if (answers.bucket !== undefined) target.bucket = answers.bucket;\n if (answers.region !== undefined) target.region = answers.region;\n if (answers.endpoint !== undefined) target.endpoint = answers.endpoint;\n if (answers.prefix !== undefined) target.prefix = answers.prefix;\n if (answers.delete !== undefined) target.delete = answers.delete;\n }\n\n return target;\n}\n\nasync function gatherSharedConfig(\n config: S3SyncConfig,\n flags: SetupFlags\n): Promise<void> {\n const questions: any[] = [];\n\n if (flags.branch === undefined) {\n questions.push({\n type: \"input\",\n name: \"branch\",\n message: \"Branch to trigger sync on:\",\n default: \"main\",\n });\n } else {\n config.branch = flags.branch;\n }\n\n if (flags.slack === undefined) {\n questions.push({\n type: \"confirm\",\n name: \"slack\",\n message: \"Enable Slack notifications?\",\n default: false,\n });\n } else {\n config.notifications.slack = flags.slack;\n }\n\n if (flags.discord === undefined) {\n questions.push({\n type: \"confirm\",\n name: \"discord\",\n message: \"Enable Discord notifications?\",\n default: false,\n });\n } else {\n config.notifications.discord = flags.discord;\n }\n\n if (questions.length > 0) {\n const answers = await inquirer.prompt(questions);\n if (answers.branch !== undefined) config.branch = answers.branch;\n if (answers.slack !== undefined) config.notifications.slack = answers.slack;\n if (answers.discord !== undefined)\n config.notifications.discord = answers.discord;\n }\n}\n\nfunction printSecretInstructions(config: S3SyncConfig): void {\n log.heading(\"Next steps\");\n log.info(\"Add these secrets to your GitHub repository:\");\n log.dim(\n \" Settings → Secrets and variables → Actions → New repository secret\\n\"\n );\n\n const secrets = [\n [\"S3_ACCESS_KEY_ID\", \"Your S3 access key\"],\n [\"S3_SECRET_ACCESS_KEY\", \"Your S3 secret key\"],\n ];\n\n if (config.notifications.slack) {\n secrets.push([\"SLACK_WEBHOOK_URL\", \"Slack incoming webhook URL\"]);\n }\n\n if (config.notifications.discord) {\n secrets.push([\"DISCORD_WEBHOOK_URL\", \"Discord webhook URL\"]);\n }\n\n for (const [name, desc] of secrets) {\n log.dim(` • ${name} — ${desc}`);\n }\n\n console.log();\n log.heading(\"Configured targets\");\n for (const t of config.targets) {\n log.info(` ${t.directory}/ → s3://${t.bucket}/${t.prefix}`);\n }\n\n console.log();\n log.success(\n `Push to \"${config.branch}\" to trigger a sync.`\n );\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { z } from \"zod\";\n\nexport interface SyncTarget {\n directory: string;\n bucket: string;\n region: string;\n endpoint: string;\n prefix: string;\n delete: boolean;\n}\n\nexport interface S3SyncConfig {\n targets: SyncTarget[];\n branch: string;\n notifications: {\n slack: boolean;\n discord: boolean;\n };\n}\n\nconst CONFIG_FILE = \"s3-sync.json\";\n\nconst syncTargetSchema: z.ZodType<SyncTarget> = z.object({\n directory: z.string().min(1),\n bucket: z.string().min(1),\n region: z.string().min(1),\n endpoint: z.string().default(\"\"),\n prefix: z.string().default(\"\"),\n delete: z.boolean().default(false),\n});\n\nconst notificationsSchema: z.ZodType<S3SyncConfig[\"notifications\"]> = z.object({\n slack: z.boolean().default(false),\n discord: z.boolean().default(false),\n});\n\nconst configSchema: z.ZodType<S3SyncConfig> = z.object({\n targets: z.array(syncTargetSchema).default([]),\n branch: z.string().min(1).default(\"main\"),\n notifications: notificationsSchema.default({ slack: false, discord: false }),\n});\n\nconst legacyConfigSchema = z.object({\n directory: z.string().min(1),\n bucket: z.string().min(1),\n region: z.string().min(1),\n endpoint: z.string().default(\"\"),\n prefix: z.string().default(\"\"),\n branch: z.string().min(1).default(\"main\"),\n delete: z.boolean().default(false),\n notifications: notificationsSchema.default({ slack: false, discord: false }),\n});\n\nexport function getConfigPath(cwd: string = process.cwd()): string {\n return path.join(cwd, CONFIG_FILE);\n}\n\nexport function configExists(cwd: string = process.cwd()): boolean {\n return existsSync(getConfigPath(cwd));\n}\n\nexport async function readConfig(\n cwd: string = process.cwd()\n): Promise<S3SyncConfig> {\n const configPath = getConfigPath(cwd);\n const raw = JSON.parse(await readFile(configPath, \"utf-8\"));\n const parsedConfig = configSchema.safeParse(raw);\n if (parsedConfig.success) return parsedConfig.data;\n\n const parsedLegacyConfig = legacyConfigSchema.safeParse(raw);\n if (!parsedLegacyConfig.success) {\n const issues = parsedConfig.error.issues\n .map((issue) => {\n const path = issue.path.length > 0 ? issue.path.join(\".\") : \"root\";\n return `${path}: ${issue.message}`;\n })\n .join(\"; \");\n throw new Error(`Invalid ${CONFIG_FILE}: ${issues}`);\n }\n\n const legacy = parsedLegacyConfig.data;\n return {\n targets: [\n {\n directory: legacy.directory,\n bucket: legacy.bucket,\n region: legacy.region,\n endpoint: legacy.endpoint,\n prefix: legacy.prefix,\n delete: legacy.delete,\n },\n ],\n branch: legacy.branch,\n notifications: legacy.notifications,\n };\n}\n\nexport async function writeConfig(\n config: S3SyncConfig,\n cwd: string = process.cwd()\n): Promise<void> {\n const configPath = getConfigPath(cwd);\n const validatedConfig = configSchema.parse(config);\n await writeFile(\n configPath,\n JSON.stringify(validatedConfig, null, 2) + \"\\n\",\n \"utf-8\"\n );\n}\n\nexport function defaultTarget(): SyncTarget {\n return {\n directory: \"public\",\n bucket: \"\",\n region: \"us-east-1\",\n endpoint: \"\",\n prefix: \"\",\n delete: false,\n };\n}\n\nexport function defaultConfig(): S3SyncConfig {\n return {\n targets: [],\n branch: \"main\",\n notifications: { slack: false, discord: false },\n };\n}\n","import type { S3SyncConfig } from \"../utils/config.js\";\n\nexport function generateWorkflow(\n config: S3SyncConfig,\n packageSpecifier: string\n): string {\n const envBlock = buildEnvBlock(config);\n const pathFilters = config.targets\n .map((t) => ` - '${t.directory}/**'`)\n .join(\"\\n\");\n\n return `name: S3 Sync\n\non:\n push:\n branches: [${config.branch}]\n paths:\n${pathFilters}\n\njobs:\n sync:\n name: Sync to S3\n runs-on: ubuntu-latest\n\n steps:\n - name: Checkout\n uses: actions/checkout@v4\n\n - name: Setup Node\n uses: actions/setup-node@v4\n with:\n node-version: \"22\"\n\n - name: Sync files\n run: npx --yes ${packageSpecifier} sync\n${envBlock}\n`;\n}\n\nfunction buildEnvBlock(config: S3SyncConfig): string {\n const lines = [\n \" env:\",\n \" S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}\",\n \" S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}\",\n ];\n\n if (config.notifications.slack) {\n lines.push(\n \" SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}\"\n );\n }\n\n if (config.notifications.discord) {\n lines.push(\n \" DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}\"\n );\n }\n\n return lines.join(\"\\n\");\n}\n","import chalk from \"chalk\";\n\nexport const log = {\n info: (msg: string) => console.log(chalk.blue(\"ℹ\"), msg),\n success: (msg: string) => console.log(chalk.green(\"✓\"), msg),\n warn: (msg: string) => console.log(chalk.yellow(\"⚠\"), msg),\n error: (msg: string) => console.error(chalk.red(\"✗\"), msg),\n dim: (msg: string) => console.log(chalk.dim(msg)),\n heading: (msg: string) => console.log(chalk.bold.cyan(`\\n${msg}\\n`)),\n};\n","import { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\ninterface PackageMeta {\n name?: string;\n version?: string;\n}\n\nfunction findPackageJson(startDirectory: string): string | undefined {\n let currentDirectory = startDirectory;\n while (true) {\n const candidate = path.join(currentDirectory, \"package.json\");\n if (existsSync(candidate)) return candidate;\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) return undefined;\n currentDirectory = parentDirectory;\n }\n}\n\nexport function getCurrentPackageSpecifier(\n fallbackName = \"@maydotinc/s3-sync\"\n): string {\n const currentFileDirectory = path.dirname(fileURLToPath(import.meta.url));\n const packageJsonPath = findPackageJson(currentFileDirectory);\n if (!packageJsonPath) return fallbackName;\n\n try {\n const parsed = JSON.parse(readFileSync(packageJsonPath, \"utf-8\")) as PackageMeta;\n const packageName = parsed.name?.trim() || fallbackName;\n const packageVersion = parsed.version?.trim();\n return packageVersion ? `${packageName}@${packageVersion}` : packageName;\n } catch {\n return fallbackName;\n }\n}\n","import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { readConfig, type SyncTarget } from \"../utils/config.js\";\nimport { log } from \"../utils/logger.js\";\nimport { fingerprintDirectory } from \"../core/fingerprint.js\";\nimport {\n createS3,\n listAllObjects,\n uploadFile,\n deleteObject,\n} from \"../core/s3-client.js\";\nimport { computeDiff, type DiffResult } from \"../core/diff.js\";\nimport { sendSlackNotification } from \"../notifications/slack.js\";\nimport { sendDiscordNotification } from \"../notifications/discord.js\";\n\nexport async function syncCommand(): Promise<void> {\n log.heading(\"s3-sync\");\n\n const config = await readConfig();\n\n const accessKeyId = process.env.S3_ACCESS_KEY_ID;\n const secretAccessKey = process.env.S3_SECRET_ACCESS_KEY;\n\n if (!accessKeyId || !secretAccessKey) {\n log.error(\n \"Missing S3_ACCESS_KEY_ID or S3_SECRET_ACCESS_KEY environment variables.\"\n );\n process.exit(1);\n }\n\n if (config.targets.length === 0) {\n log.error(\"No sync targets configured. Run setup first.\");\n process.exit(1);\n }\n\n const summaries: string[] = [];\n let hasErrors = false;\n const globalEndpoint = process.env.S3_ENDPOINT?.trim() || undefined;\n const hasTargetSpecificEndpoints = config.targets.some(\n (target) => target.endpoint.trim().length > 0\n );\n\n for (const target of config.targets) {\n try {\n const endpoint = resolveEndpoint(\n target.endpoint,\n globalEndpoint,\n hasTargetSpecificEndpoints\n );\n const diff = await syncTarget(\n target,\n accessKeyId,\n secretAccessKey,\n endpoint\n );\n summaries.push(buildTargetSummary(target, diff));\n } catch (err: any) {\n log.error(`[${target.directory}] ${err.message}`);\n hasErrors = true;\n }\n }\n\n if (summaries.length > 0) {\n const fullSummary = summaries.join(\"\\n\\n---\\n\\n\");\n await notify(config, fullSummary);\n }\n\n if (hasErrors) {\n process.exit(1);\n }\n\n log.success(\"All targets synced.\");\n}\n\nasync function syncTarget(\n target: SyncTarget,\n accessKeyId: string,\n secretAccessKey: string,\n endpoint: string | undefined\n): Promise<DiffResult> {\n log.heading(`Syncing ${target.directory}/ → s3://${target.bucket}/${target.prefix}`);\n\n const directory = path.resolve(target.directory);\n if (!existsSync(directory)) {\n throw new Error(`Directory \"${target.directory}\" does not exist.`);\n }\n\n const client = createS3({\n bucket: target.bucket,\n region: target.region,\n endpoint,\n accessKeyId,\n secretAccessKey,\n });\n\n log.info(`Scanning ${target.directory}/...`);\n const localFiles = await fingerprintDirectory(directory);\n log.info(`Found ${localFiles.length} local files`);\n\n log.info(`Listing objects in s3://${target.bucket}/${target.prefix}...`);\n const remoteObjects = await listAllObjects(\n client,\n target.bucket,\n target.prefix\n );\n log.info(`Found ${remoteObjects.length} remote objects`);\n\n const diff = computeDiff(\n localFiles,\n remoteObjects,\n target.prefix,\n target.delete\n );\n\n log.info(\n `${diff.toUpload.length} to upload, ${diff.toDelete.length} to delete, ${diff.unchanged.length} unchanged`\n );\n\n if (diff.toUpload.length === 0 && diff.toDelete.length === 0) {\n log.success(`[${target.directory}] Already in sync.`);\n return diff;\n }\n\n for (const file of diff.toUpload) {\n const key = target.prefix\n ? `${target.prefix}/${file.relativePath}`\n : file.relativePath;\n log.dim(` uploading ${key}`);\n await uploadFile(client, target.bucket, key, file.absolutePath);\n }\n\n if (diff.toUpload.length > 0) {\n log.success(`[${target.directory}] Uploaded ${diff.toUpload.length} files`);\n }\n\n for (const obj of diff.toDelete) {\n log.dim(` deleting ${obj.key}`);\n await deleteObject(client, target.bucket, obj.key);\n }\n\n if (diff.toDelete.length > 0) {\n log.success(`[${target.directory}] Deleted ${diff.toDelete.length} files`);\n }\n\n return diff;\n}\n\nfunction resolveEndpoint(\n targetEndpoint: string,\n globalEndpoint: string | undefined,\n hasTargetSpecificEndpoints: boolean\n): string | undefined {\n const trimmedTargetEndpoint = targetEndpoint.trim();\n if (trimmedTargetEndpoint) return trimmedTargetEndpoint;\n if (!hasTargetSpecificEndpoints) return globalEndpoint;\n return undefined;\n}\n\nfunction buildTargetSummary(target: SyncTarget, diff: DiffResult): string {\n const lines = [\n `*${target.directory}/ → s3://${target.bucket}/${target.prefix}*`,\n `Uploaded: ${diff.toUpload.length}`,\n `Deleted: ${diff.toDelete.length}`,\n `Unchanged: ${diff.unchanged.length}`,\n ];\n\n if (diff.toUpload.length > 0) {\n const fileList = diff.toUpload\n .slice(0, 10)\n .map((f) => ` • ${f.relativePath}`)\n .join(\"\\n\");\n lines.push(`\\nUploaded files:\\n${fileList}`);\n if (diff.toUpload.length > 10) {\n lines.push(` ...and ${diff.toUpload.length - 10} more`);\n }\n }\n\n return lines.join(\"\\n\");\n}\n\nasync function notify(config: any, message: string): Promise<void> {\n if (config.notifications?.slack) {\n const webhookUrl = process.env.SLACK_WEBHOOK_URL;\n if (webhookUrl) {\n try {\n await sendSlackNotification(webhookUrl, message);\n log.success(\"Slack notification sent\");\n } catch (err: any) {\n log.warn(`Slack notification failed: ${err.message}`);\n }\n }\n }\n\n if (config.notifications?.discord) {\n const webhookUrl = process.env.DISCORD_WEBHOOK_URL;\n if (webhookUrl) {\n try {\n await sendDiscordNotification(webhookUrl, message);\n log.success(\"Discord notification sent\");\n } catch (err: any) {\n log.warn(`Discord notification failed: ${err.message}`);\n }\n }\n }\n}\n","import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { glob } from \"glob\";\n\nexport interface LocalFile {\n relativePath: string;\n absolutePath: string;\n md5: string;\n size: number;\n}\n\nexport async function fingerprintDirectory(\n directory: string\n): Promise<LocalFile[]> {\n const absoluteDir = path.resolve(directory);\n const files = await glob(\"**/*\", {\n cwd: absoluteDir,\n nodir: true,\n dot: false,\n });\n\n const results: LocalFile[] = [];\n\n await Promise.all(\n files.map(async (relativePath) => {\n const absolutePath = path.join(absoluteDir, relativePath);\n const content = await readFile(absolutePath);\n const md5 = createHash(\"md5\").update(content).digest(\"hex\");\n\n results.push({\n relativePath: relativePath.replace(/\\\\/g, \"/\"),\n absolutePath,\n md5,\n size: content.length,\n });\n })\n );\n\n return results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));\n}\n","import {\n S3Client,\n ListObjectsV2Command,\n type ListObjectsV2CommandOutput,\n PutObjectCommand,\n DeleteObjectCommand,\n} from \"@aws-sdk/client-s3\";\nimport { readFile } from \"node:fs/promises\";\nimport mime from \"mime-types\";\n\nexport interface S3Object {\n key: string;\n etag: string;\n size: number;\n}\n\nexport interface S3Config {\n bucket: string;\n region: string;\n endpoint?: string;\n accessKeyId: string;\n secretAccessKey: string;\n}\n\nexport function createS3(config: S3Config): S3Client {\n return new S3Client({\n region: config.region,\n credentials: {\n accessKeyId: config.accessKeyId,\n secretAccessKey: config.secretAccessKey,\n },\n ...(config.endpoint && {\n endpoint: config.endpoint,\n forcePathStyle: true,\n }),\n });\n}\n\nexport async function listAllObjects(\n client: S3Client,\n bucket: string,\n prefix: string\n): Promise<S3Object[]> {\n const objects: S3Object[] = [];\n let continuationToken: string | undefined;\n\n do {\n const command = new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: prefix || undefined,\n ContinuationToken: continuationToken,\n });\n\n const response: ListObjectsV2CommandOutput = await client.send(command);\n\n if (response.Contents) {\n for (const obj of response.Contents) {\n if (obj.Key && obj.ETag && obj.Size !== undefined) {\n objects.push({\n key: obj.Key,\n etag: obj.ETag.replace(/\"/g, \"\"),\n size: obj.Size,\n });\n }\n }\n }\n\n continuationToken = response.IsTruncated\n ? response.NextContinuationToken\n : undefined;\n } while (continuationToken);\n\n return objects;\n}\n\nexport async function uploadFile(\n client: S3Client,\n bucket: string,\n key: string,\n filePath: string\n): Promise<void> {\n const body = await readFile(filePath);\n const contentType = mime.lookup(filePath) || \"application/octet-stream\";\n\n await client.send(\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n Body: body,\n ContentType: contentType,\n })\n );\n}\n\nexport async function deleteObject(\n client: S3Client,\n bucket: string,\n key: string\n): Promise<void> {\n await client.send(\n new DeleteObjectCommand({\n Bucket: bucket,\n Key: key,\n })\n );\n}\n","import type { LocalFile } from \"./fingerprint.js\";\nimport type { S3Object } from \"./s3-client.js\";\n\nexport interface DiffResult {\n toUpload: LocalFile[];\n toDelete: S3Object[];\n unchanged: LocalFile[];\n}\n\nexport function computeDiff(\n localFiles: LocalFile[],\n remoteObjects: S3Object[],\n prefix: string,\n shouldDelete: boolean\n): DiffResult {\n const remoteByKey = new Map<string, S3Object>();\n for (const obj of remoteObjects) {\n remoteByKey.set(obj.key, obj);\n }\n\n const toUpload: LocalFile[] = [];\n const unchanged: LocalFile[] = [];\n const localKeys = new Set<string>();\n\n for (const file of localFiles) {\n const key = prefix ? `${prefix}/${file.relativePath}` : file.relativePath;\n localKeys.add(key);\n\n const remote = remoteByKey.get(key);\n\n if (!remote || remote.etag !== file.md5) {\n toUpload.push(file);\n } else {\n unchanged.push(file);\n }\n }\n\n const toDelete: S3Object[] = [];\n if (shouldDelete) {\n for (const obj of remoteObjects) {\n if (!localKeys.has(obj.key)) {\n toDelete.push(obj);\n }\n }\n }\n\n return { toUpload, toDelete, unchanged };\n}\n","export async function sendSlackNotification(\n webhookUrl: string,\n message: string\n): Promise<void> {\n const response = await fetch(webhookUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ text: message }),\n });\n\n if (!response.ok) {\n throw new Error(`Slack notification failed: ${response.statusText}`);\n }\n}\n","export async function sendDiscordNotification(\n webhookUrl: string,\n message: string\n): Promise<void> {\n const response = await fetch(webhookUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ content: message }),\n });\n\n if (!response.ok) {\n throw new Error(`Discord notification failed: ${response.statusText}`);\n }\n}\n"],"mappings":"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","names":["writeFile","existsSync","path","path","existsSync","path","path","existsSync","writeFile","existsSync","path","readFile","path","readFile","path","existsSync"]}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@maydotinc/s3-sync",
3
+ "version": "0.1.0",
4
+ "description": "CLI to sync local directories to S3-compatible buckets via GitHub Actions",
5
+ "type": "module",
6
+ "bin": {
7
+ "s3-sync": "./dist/index.js"
8
+ },
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsup",
17
+ "dev": "tsx src/index.ts",
18
+ "typecheck": "tsc --noEmit",
19
+ "prepublishOnly": "npm run build && npm run typecheck"
20
+ },
21
+ "keywords": [
22
+ "s3",
23
+ "sync",
24
+ "upload",
25
+ "github-actions",
26
+ "cdn",
27
+ "static-assets"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "@aws-sdk/client-s3": "^3.700.0",
32
+ "chalk": "^5.4.0",
33
+ "commander": "^13.0.0",
34
+ "glob": "^11.0.0",
35
+ "inquirer": "^12.3.0",
36
+ "mime-types": "^2.1.35",
37
+ "zod": "^4.3.6"
38
+ },
39
+ "devDependencies": {
40
+ "@types/inquirer": "^9.0.7",
41
+ "@types/mime-types": "^2.1.4",
42
+ "@types/node": "^22.0.0",
43
+ "tsup": "^8.3.0",
44
+ "tsx": "^4.19.0",
45
+ "typescript": "^5.7.0"
46
+ }
47
+ }