afterpack 0.2.3 → 0.2.4

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 (4) hide show
  1. package/README.md +24 -4
  2. package/SKILL.md +26 -6
  3. package/dist/cli.js +100 -51
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -31,13 +31,27 @@ Nested `node_modules/` folders are skipped. Pass [`--paths.include`][paths.inclu
31
31
 
32
32
  Each run writes:
33
33
 
34
- - `.afterpack-protection.json` in the output directory, the receipt that `afterpack verify` checks.
34
+ - `.afterpack/receipts/<output dir>.json` (`.afterpack/receipts/dist.json` for `dist`), the receipt
35
+ that `afterpack verify` checks.
35
36
  - `.afterpack/backup/`, a copy of the original files, so `afterpack restore` can undo the run.
36
37
  Turn it off with [`--build.backup`][build.backup]`=false`.
37
38
  - `.afterpack/protectionMap.html`, the [Protection Map](https://www.afterpack.dev/docs/protection-map),
38
39
  when your build has source maps.
39
- - `.map` files next to the output, when your build has source maps and the run is not a production
40
- build.
40
+ - `.map` files next to the output, only when you ask for them with
41
+ [`--sourceMap.enabled`][sourceMap.enabled] or run with `--build.mode=development` (or
42
+ `NODE_ENV=development`) on a build that has source maps.
43
+
44
+ Every other run is a production run: it deletes your build's own source maps, hidden ones included,
45
+ and the `sourceMappingURL` comments that name them, so no original source ships beside the output.
46
+
47
+ `.afterpack/` sits next to your project's lockfile, `afterpack.json` or `.git` (failing those, its
48
+ `package.json`), otherwise beside the build directory, never inside it: `cd public && npx afterpack
49
+ .` keeps it beside `public/`, and a `package.json` your build emits, such as Nitro's
50
+ `.output/server/package.json`, never moves it. A single file in a project root that holds a
51
+ lockfile, `afterpack.json` or `.git` keeps it in that root. The same place holds every run's files
52
+ whatever directory you run from, so a second run over the same output, or over the same file, is
53
+ refused from anywhere. A run fails if it finds a `.afterpack/` inside the output, and removes a
54
+ `.afterpack-protection.json` an older version left there.
41
55
 
42
56
  The backup and the Protection Map contain your original source. `.afterpack/` carries its own
43
57
  `.gitignore` and self-ignores. Never deploy or commit them.
@@ -49,13 +63,18 @@ runs. It never prompts.
49
63
 
50
64
  ## `afterpack verify [dir]`
51
65
 
52
- Checks a build against its protection receipt. Run it in your deploy step.
66
+ Checks a build against its protection receipt in `.afterpack/receipts/`. Run it in your deploy
67
+ step.
53
68
 
54
69
  ```sh
55
70
  npx afterpack@latest verify . # looks in ./ and ./.next/
56
71
  npx afterpack@latest verify dist
72
+ npx afterpack@latest verify build # SvelteKit's adapter copy; also .output/public, out/
57
73
  ```
58
74
 
75
+ A directory a framework copied from its build output, such as SvelteKit's `build/`, Nuxt's
76
+ `.output/public/` or Next's `out/`, is checked against the receipt of the directory it copied.
77
+
59
78
  It exits `1` when the receipt is missing, belongs to a different build, or a file changed after it
60
79
  was obfuscated. The CLI and the plugins for Vite, Next.js, webpack, Rollup, esbuild and Angular all
61
80
  write a receipt.
@@ -104,6 +123,7 @@ the [configuration reference](https://www.afterpack.dev/docs/config). The
104
123
  [identifiers.reserved]: https://www.afterpack.dev/docs/config#identifiers-reserved
105
124
  [build.backup]: https://www.afterpack.dev/docs/config#build-backup
106
125
  [protectionMap.enabled]: https://www.afterpack.dev/docs/config#protectionMap-enabled
126
+ [sourceMap.enabled]: https://www.afterpack.dev/docs/config#sourceMap-enabled
107
127
  [diagnostics.format]: https://www.afterpack.dev/docs/config#diagnostics-format
108
128
  [inflation.max]: https://www.afterpack.dev/docs/config#inflation-max
109
129
  [key]: https://www.afterpack.dev/docs/config#key
package/SKILL.md CHANGED
@@ -67,11 +67,26 @@ Rules:
67
67
 
68
68
  ## What a run writes
69
69
 
70
- - `.afterpack-protection.json` in the output directory: the receipt `afterpack verify` checks.
70
+ - `.afterpack/receipts/<output dir>.json` (`.afterpack/receipts/dist.json` for `dist`): the receipt
71
+ `afterpack verify` checks.
71
72
  - `.afterpack/backup/`: the original files, on by default for the CLI, so `afterpack restore` can
72
73
  undo the run. `--build.backup=false` turns it off.
73
74
  - `.afterpack/protectionMap.html`: the Protection Map report, when the build has source maps.
74
- - `.map` files beside the output, when an input map exists and the run is not a production build.
75
+ - `.map` files beside the output, only with `--sourceMap.enabled` or on a development run
76
+ (`--build.mode=development` or `NODE_ENV=development`) when an input map exists.
77
+
78
+ Every other run is a production run, `CI` and an unset `NODE_ENV` included. It deletes the build's
79
+ own source maps, hidden ones included, and the `sourceMappingURL` comments that name them.
80
+
81
+ Every front door puts `.afterpack/` next to the nearest lockfile, `afterpack.json` or `.git` above
82
+ the build directory, failing those the nearest `package.json`, otherwise beside the build
83
+ directory, never inside it (`cd public && npx afterpack .` keeps it beside `public/`). A
84
+ `package.json` the build emits, such as Nitro's `.output/server/package.json` or a dual package's
85
+ `dist/cjs/package.json`, never moves it. For a single file, the file's own directory counts when it
86
+ holds a lockfile, `afterpack.json` or `.git`, so `npx afterpack app.js` in such a root keeps
87
+ `.afterpack/` there. The place does not depend on the working directory, so a re-run over the same
88
+ output or the same file is refused from anywhere. A run fails if it finds a `.afterpack/` inside
89
+ the output, and removes a `.afterpack-protection.json` an older version left there.
75
90
 
76
91
  The backup and the Protection Map contain the original source. `.afterpack/` carries its own
77
92
  `.gitignore` and self-ignores. Never deploy, publish or commit them.
@@ -103,7 +118,7 @@ minification.
103
118
  | [`paths.include`][paths.include] | globs to add back to the walk | `[]` |
104
119
  | [`identifiers.reserved`][identifiers.reserved] | names never to rename | `[]` |
105
120
  | [`build.backup`][build.backup] | keep originals in `.afterpack/backup/` | `true` for the CLI, `false` in plugins |
106
- | [`sourceMap.enabled`][sourceMap.enabled] | write `.map` files | on when an input map exists, off in production |
121
+ | [`sourceMap.enabled`][sourceMap.enabled] | write `.map` files | off; on in development mode when an input map exists |
107
122
  | [`protectionMap.enabled`][protectionMap.enabled] | write the Protection Map | on when an input map exists |
108
123
  | [`diagnostics.format`][diagnostics.format] | `text` or `json` | `text` |
109
124
  | [`diagnostics.level`][diagnostics.level] | `summary`, `all` or `none` (errors still print) | `summary` |
@@ -194,15 +209,20 @@ Details: https://www.afterpack.dev/docs/pro
194
209
 
195
210
  ## `afterpack verify [dir]`
196
211
 
197
- Add this to the deploy step. It re-hashes every file the protection receipt names.
212
+ Add this to the deploy step. It reads the receipt for `[dir]` from `.afterpack/receipts/`, found
213
+ by the same rule as above, and re-hashes every file that receipt names.
198
214
 
199
215
  ```bash
200
216
  npx afterpack@latest verify . # looks in ./ and ./.next/
201
217
  npx afterpack@latest verify dist
218
+ npx afterpack@latest verify build # SvelteKit's adapter copy; also .output/public, out/
202
219
  ```
203
220
 
204
- It exits `1` when the receipt is missing, belongs to a different build, or a file changed after it
205
- was obfuscated. A missing receipt is a failure. `verify` reads no configuration and takes only
221
+ When `[dir]` has no receipt of its own, verify looks for the project receipt whose files `[dir]`
222
+ holds, at the same relative paths or under one prefix, so a framework's copy of its build output
223
+ (SvelteKit's `build/`, Nuxt's `.output/public/`, Next's `out/`) is checked against the receipt of
224
+ the directory it copied. It exits `1` when the receipt is missing, belongs to a different build,
225
+ or a file changed after it was obfuscated. A missing receipt is a failure. `verify` reads no configuration and takes only
206
226
  `--diagnostics.format` and `--diagnostics.level`.
207
227
 
208
228
  ## `afterpack restore [dir]`
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { processBatch, version } from "@afterpack/core";
4
- import { AlreadyObfuscatedError, CLOUD_REFUSAL_SUMMARY, CONFIG_KEYS, CORE_FLOOR_SUMMARY, CloudApiError, CoreVersionError, DIAG_ALREADY_OBFUSCATED, PROTECTION_RECEIPT_FILE, UnreadableReceiptError, collectJsFiles, createTelemetryReporter, ensureAfterpackGitignore, findUpward, getPath, installedRequiredLine, loadConfigFile, mergeConfig, npxAlternative, parseCliOptions, parseEnvOptions, resolveClientIdentity, resolvePluginConfig, runObfuscationPass, serverAddsInfo, sha256Of, toEngineConfig, toPluginOptions, verifyProtectionReceipt } from "@afterpack/integration-utils";
4
+ import { AFTERPACK_DIR, AfterpackFilesError, AlreadyObfuscatedError, CLOUD_REFUSAL_SUMMARY, CONFIG_KEYS, CORE_FLOOR_SUMMARY, CloudApiError, CoreVersionError, DIAG_ALREADY_OBFUSCATED, PROTECTION_RECEIPTS_DIR, UnreadableReceiptError, afterpackHome, collectJsFiles, createTelemetryReporter, ensureAfterpackGitignore, getPath, installedRequiredLine, loadConfigFile, locateProtectionReceipt, mergeConfig, npxAlternative, parseCliOptions, parseEnvOptions, protectionReceiptPath, resolveClientIdentity, resolvePluginConfig, runObfuscationPass, serverAddsInfo, sha256Of, toEngineConfig, toPluginOptions, verifyProtectionReceipt } from "@afterpack/integration-utils";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
6
  import { createHash } from "node:crypto";
7
7
 
@@ -490,6 +490,13 @@ function reportingLogger(logger, mode) {
490
490
  error: (m) => logger.error(m)
491
491
  };
492
492
  }
493
+ function realDir(path) {
494
+ try {
495
+ return realpathSync.native(path);
496
+ } catch {
497
+ return path;
498
+ }
499
+ }
493
500
  function documentPath(cwd, filePath) {
494
501
  const rel = relative(cwd, filePath);
495
502
  if (rel === "") return ".";
@@ -646,12 +653,19 @@ result, not a failure), 1 when the scan failed or the stream ended early,
646
653
  ${dim(CONTACT_FOOTER)}`;
647
654
  const DEFAULT_API_BASE = "https://api.afterpack.dev";
648
655
  const WEB_BASE = "https://www.afterpack.dev";
649
- function normalizeUrl(input) {
656
+ const LOCAL_PATH = /^(?:[\\/.~]|[A-Za-z]:[\\/]|file:)/i;
657
+ function isNamedHost(hostname) {
658
+ if (hostname === "localhost" || hostname.startsWith("[")) return true;
659
+ return hostname.includes(".");
660
+ }
661
+ function normalizeUrl(input, cwd) {
650
662
  const raw = input.trim();
663
+ if (LOCAL_PATH.test(raw) || existsSync(resolve(cwd, raw))) return null;
651
664
  const candidate = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
652
665
  try {
653
666
  const parsed = new URL(candidate);
654
- return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString() : null;
667
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
668
+ return isNamedHost(parsed.hostname) ? parsed.toString() : null;
655
669
  } catch {
656
670
  return null;
657
671
  }
@@ -779,7 +793,7 @@ function misuse(deps, code, message) {
779
793
  async function audit(deps) {
780
794
  if (deps.positionals.length === 0) return misuse(deps, "MISSING_URL", "audit needs a <url>");
781
795
  if (deps.positionals.length > 1) return misuse(deps, "TOO_MANY_URLS", `audit takes a single <url>, got ${deps.positionals.length}`);
782
- const url = normalizeUrl(deps.positionals[0]);
796
+ const url = normalizeUrl(deps.positionals[0], deps.cwd ?? process.cwd());
783
797
  if (!url) return misuse(deps, "INVALID_URL", `not a URL: ${deps.positionals[0]}`);
784
798
  const interactive = deps.mode.format !== "json" && deps.stdout.isTTY;
785
799
  deps.report.log(header(deps.version));
@@ -873,19 +887,22 @@ async function audit(deps) {
873
887
  //#endregion
874
888
  //#region src/backup.ts
875
889
  const BACKUP_MANIFEST_FILE = "manifest.json";
876
- const BACKUP_MANIFEST_RELATIVE = join(".afterpack", "backup", BACKUP_MANIFEST_FILE);
877
- function projectRootFor(cwd, configFile) {
878
- return configFile ? dirname(configFile) : cwd;
879
- }
880
890
  function backupDir(projectRoot) {
881
- return join(projectRoot, ".afterpack", "backup");
891
+ return join(projectRoot, AFTERPACK_DIR, "backup");
882
892
  }
883
893
  function backupManifestPath(projectRoot) {
884
894
  return join(backupDir(projectRoot), BACKUP_MANIFEST_FILE);
885
895
  }
886
- function findBackupProjectRoot(startDir) {
887
- const manifestPath = findUpward(startDir, BACKUP_MANIFEST_RELATIVE, { stopAtDir: (dir) => existsSync(join(dir, ".git")) });
888
- return manifestPath ? dirname(dirname(dirname(manifestPath))) : null;
896
+ function homeProjectRoot(dir) {
897
+ try {
898
+ return afterpackHome(dir).projectRoot;
899
+ } catch {
900
+ return null;
901
+ }
902
+ }
903
+ function findBackupProjectRoot(dir) {
904
+ const candidates$1 = [dir, homeProjectRoot(dir)].filter((c) => c !== null);
905
+ return candidates$1.find((c) => existsSync(backupManifestPath(c))) ?? null;
889
906
  }
890
907
  function toPosixPath(value) {
891
908
  return value.split(sep).join("/");
@@ -936,13 +953,15 @@ function readBackupManifest(projectRoot) {
936
953
  }
937
954
  function writeBackups(input) {
938
955
  const dir = backupDir(input.projectRoot);
939
- rmSync(dir, {
956
+ const previous = input.merge ? readBackupManifest(input.projectRoot) : null;
957
+ const replaced = new Set(input.pending.map((p) => p.relPath));
958
+ const files = previous?.status === "ok" && previous.manifest.protectedRoot === input.protectedRoot ? previous.manifest.files.filter((f) => !replaced.has(f.path)) : [];
959
+ if (files.length === 0) rmSync(dir, {
940
960
  recursive: true,
941
961
  force: true
942
962
  });
943
963
  mkdirSync(dir, { recursive: true });
944
964
  ensureAfterpackGitignore(dir);
945
- const files = [];
946
965
  for (const pending of input.pending) {
947
966
  const dest = absoluteFilePath(dir, pending.relPath);
948
967
  mkdirSync(dirname(dest), { recursive: true });
@@ -1146,14 +1165,15 @@ const RESTORE_HELP = `${RESTORE_USAGE}
1146
1165
  Restores the original, pre-obfuscation files that a previous \`afterpack\` run backed up to
1147
1166
  .afterpack/backup/, undoing that run in place.
1148
1167
 
1149
- Searches upward from [dir] (or the working directory) for the project root that holds
1150
- .afterpack/backup/ — the same directory the \`afterpack\` run was launched from — so restore
1151
- works whether it is pointed at that root or at the build directory beneath it.
1168
+ Finds .afterpack/backup/ where the \`afterpack\` run kept it: next to the nearest lockfile,
1169
+ afterpack.json or .git above the build directory (failing those the nearest package.json,
1170
+ otherwise beside the build directory). Point it at that project root or at the build directory
1171
+ itself.
1152
1172
 
1153
1173
  Verifies every recorded file against the hash it was obfuscated to BEFORE touching anything: if
1154
1174
  any file no longer matches, or the manifest itself is unreadable, restore changes nothing and
1155
- fails closed. On success it deletes the backup and the stale protection receipt it recorded,
1156
- leaving .afterpack/ itself in place.
1175
+ fails closed. On success it deletes the backup and the stale protection receipt it recorded in
1176
+ .afterpack/receipts/, leaving .afterpack/ itself in place.
1157
1177
 
1158
1178
  Arguments:
1159
1179
  [dir] a directory at or beneath the project root that holds
@@ -1172,7 +1192,8 @@ function fail$1(deps, exitCode, code, message, fix, detail = []) {
1172
1192
  return commandFailure(deps, "restore", exitCode, code, message, fix, detail);
1173
1193
  }
1174
1194
  function isRecordedReceipt(projectRoot, receiptPath) {
1175
- if (basename(receiptPath) !== PROTECTION_RECEIPT_FILE) return false;
1195
+ if (basename(dirname(receiptPath)) !== PROTECTION_RECEIPTS_DIR) return false;
1196
+ if (!receiptPath.endsWith(".json")) return false;
1176
1197
  const rel = relative(projectRoot, receiptPath);
1177
1198
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel) && existsSync(receiptPath);
1178
1199
  }
@@ -1181,9 +1202,9 @@ function restore(deps) {
1181
1202
  const { cwd, positionals } = deps;
1182
1203
  if (positionals.length > 1) return fail$1(deps, EXIT.usage, "TOO_MANY_ARGUMENTS", `expected a single [dir], got ${positionals.length}`, RESTORE_USAGE);
1183
1204
  const requested = positionals[0] ?? ".";
1184
- const target = isAbsolute(requested) ? requested : resolve(cwd, requested);
1185
- if (!existsSync(target)) return fail$1(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point restore at the project root that holds .afterpack/backup/, or a directory beneath it.");
1186
- const projectRoot = findBackupProjectRoot(target);
1205
+ const requestedPath = isAbsolute(requested) ? requested : resolve(cwd, requested);
1206
+ if (!existsSync(requestedPath)) return fail$1(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point restore at the project root that holds .afterpack/backup/, or a directory beneath it.");
1207
+ const projectRoot = findBackupProjectRoot(realDir(requestedPath));
1187
1208
  if (projectRoot === null) return fail$1(deps, EXIT.failure, "NOTHING_TO_RESTORE", "Nothing to restore", NOTHING_TO_RESTORE_FIX);
1188
1209
  const manifestResult = readBackupManifest(projectRoot);
1189
1210
  if (manifestResult.status === "corrupt") return fail$1(deps, EXIT.failure, "BACKUP_MANIFEST_CORRUPT", "Backup manifest is unreadable", `Delete ${documentPath(cwd, manifestResult.path)} and run \`afterpack <path>\` again — a corrupt manifest cannot be restored from.`);
@@ -1354,15 +1375,18 @@ const VERIFY_USAGE = "usage: afterpack verify [dir] [--diagnostics.format=text|j
1354
1375
  const VERIFY_HELP = `${VERIFY_USAGE}
1355
1376
 
1356
1377
  Re-checks a build output tree against the protection receipt the build wrote
1357
- into it: every recorded file must still hash to the value it was obfuscated to.
1358
- Run it in the deploy step, after the build and before the upload.
1378
+ for it in .afterpack/receipts/, next to the nearest lockfile, afterpack.json or
1379
+ .git above the tree (failing those the nearest package.json, otherwise beside
1380
+ the tree): every recorded file must still
1381
+ hash to the value it was obfuscated to. Run it in the deploy step, after the
1382
+ build and before the upload.
1359
1383
 
1360
1384
  A MISSING receipt FAILS. This command is only ever run by someone who expects a
1361
1385
  protected tree, so "there was nothing to check" is not a pass.
1362
1386
 
1363
1387
  Arguments:
1364
- [dir] the build output, or a project root whose .next/
1365
- holds the receipt (default: the working directory)
1388
+ [dir] the build output, or a Next.js project root whose
1389
+ .next/ was protected (default: the working directory)
1366
1390
 
1367
1391
  Options:
1368
1392
  --diagnostics.format=<text|json> json prints ONE document on stdout
@@ -1398,11 +1422,17 @@ function verify(deps) {
1398
1422
  const { cwd, positionals } = deps;
1399
1423
  if (positionals.length > 1) return fail(deps, EXIT.usage, "TOO_MANY_ARGUMENTS", `expected a single [dir], got ${positionals.length}`, VERIFY_USAGE);
1400
1424
  const requested = positionals[0] ?? ".";
1401
- const target = isAbsolute(requested) ? requested : resolve(cwd, requested);
1402
- if (!existsSync(target)) return fail(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point verify at the build output directory, or at the project root above it.");
1403
- const dir = candidates(target).find((c) => existsSync(join(c, PROTECTION_RECEIPT_FILE)));
1404
- if (!dir) return fail(deps, EXIT.failure, "RECEIPT_MISSING", `no protection receipt found for ${requested}`, "A receipt is written by the run that obfuscated the tree — run `afterpack <dir>`, or build again through the framework plugin, then point verify at that output directory.", candidates(target).map((c) => `looked in ${documentPath(cwd, join(c, PROTECTION_RECEIPT_FILE))}`));
1405
- const { receipt, problems } = verifyProtectionReceipt(dir, readBuildId(dir));
1425
+ const requestedPath = isAbsolute(requested) ? requested : resolve(cwd, requested);
1426
+ if (!existsSync(requestedPath)) return fail(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point verify at the build output directory, or at the project root above it.");
1427
+ const target = realDir(requestedPath);
1428
+ const dirs = candidates(target).filter((c) => existsSync(c));
1429
+ const located = dirs.map((c) => locateProtectionReceipt(c, { copies: false })).find((l) => l !== null) ?? dirs.map((c) => locateProtectionReceipt(c)).find((l) => l !== null);
1430
+ if (!located) {
1431
+ const looked = dirs.flatMap((c) => [...new Set([protectionReceiptPath(c), protectionReceiptPath(afterpackHome(c, { fileTarget: true }))])]);
1432
+ return fail(deps, EXIT.failure, "RECEIPT_MISSING", `no protection receipt found for ${requested}`, "A receipt is written to .afterpack/receipts/ by the run that obfuscated the tree — run `afterpack <dir>`, or build again through the framework plugin, then point verify at that output directory.", [...looked.map((path) => `looked in ${documentPath(cwd, path)}`), "and for a receipt whose files this directory holds a copy of"]);
1433
+ }
1434
+ const { dir, receiptPath, copiedFrom } = located;
1435
+ const { receipt, problems } = verifyProtectionReceipt(receiptPath, dir, readBuildId(dir));
1406
1436
  if (problems.length > 0) return fail(deps, EXIT.failure, "RECEIPT_MISMATCH", `${documentPath(cwd, dir)} FAILED verification: ${problems.join("; ")}`, "Rebuild the tree with the AfterPack front door and verify the fresh output; never deploy a tree whose receipt does not match.", problems);
1407
1437
  const files = receipt?.files ?? [];
1408
1438
  if (deps.mode.format === "json") {
@@ -1421,6 +1451,8 @@ function verify(deps) {
1421
1451
  diagnostics: [],
1422
1452
  summary: {
1423
1453
  directory: documentPath(cwd, dir),
1454
+ receipt: documentPath(cwd, receiptPath),
1455
+ copiedFrom: copiedFrom === null ? null : documentPath(cwd, copiedFrom),
1424
1456
  tool: receipt?.tool ?? null,
1425
1457
  engine: receipt?.engine ?? null,
1426
1458
  bundler: receipt?.bundler ?? null,
@@ -1435,7 +1467,7 @@ function verify(deps) {
1435
1467
  return EXIT.ok;
1436
1468
  }
1437
1469
  const engine = receipt?.engine === "cloud" ? " · AfterPack Pro" : "";
1438
- deps.report.log(`${green("✓")} Verified ${files.length} ${files.length === 1 ? "file" : "files"} · ` + displayDir(deps.cwd, dir) + engine);
1470
+ deps.report.log(`${green("✓")} Verified ${files.length} ${files.length === 1 ? "file" : "files"} · ` + displayDir(deps.cwd, dir) + (copiedFrom === null ? "" : dim(` · copy of ${displayDir(deps.cwd, copiedFrom)}`)) + engine);
1439
1471
  return EXIT.ok;
1440
1472
  }
1441
1473
 
@@ -1460,15 +1492,10 @@ function unknownCommand(argv, cwd) {
1460
1492
  };
1461
1493
  return null;
1462
1494
  }
1463
- function observeEngine(engine, into) {
1464
- return {
1465
- processBatch: async (inputs, config, buildContext) => {
1466
- const bytesIn = new Map(inputs.map((i) => [i.path, Buffer.byteLength(i.source)]));
1467
- const batch = await engine.processBatch(inputs, config, buildContext);
1468
- for (const file of batch.files) into.push(captureFile(file, bytesIn.get(file.path) ?? 0));
1469
- return batch;
1470
- },
1471
- ...engine.version ? { version: () => engine.version() } : {}
1495
+ function captureBatch(into) {
1496
+ return (inputs, batch) => {
1497
+ const bytesIn = new Map(inputs.map((i) => [i.path, Buffer.byteLength(i.source)]));
1498
+ for (const file of batch.files) into.push(captureFile(file, bytesIn.get(file.path) ?? 0));
1472
1499
  };
1473
1500
  }
1474
1501
  function captureFile(file, bytesIn) {
@@ -1712,14 +1739,16 @@ async function runObfuscationAndBackup(input) {
1712
1739
  label: input.label,
1713
1740
  work: () => runObfuscationPass({
1714
1741
  files: input.files,
1715
- engine: observeEngine(input.engine, captured),
1742
+ engine: input.engine,
1743
+ onBatch: captureBatch(captured),
1716
1744
  label: "afterpack",
1717
1745
  cwd: input.cwd,
1718
1746
  startedAt: input.startedAt,
1719
1747
  combinedProtectionMap: {
1720
1748
  buildDir: input.buildDir,
1721
- afterpackDir: join(input.cwd, ".afterpack")
1749
+ fileTarget: input.fileTarget
1722
1750
  },
1751
+ sourceMapSweepDir: input.sweepSourceMaps ? input.buildDir : null,
1723
1752
  artifactOptions: {
1724
1753
  ...input.parsed.artifactOptions,
1725
1754
  build: {
@@ -1753,7 +1782,8 @@ async function runObfuscationAndBackup(input) {
1753
1782
  protectedRoot: input.buildDir,
1754
1783
  cliVersion: input.version,
1755
1784
  pending: input.pendingBackups,
1756
- receiptPath: result?.receiptPath ?? null
1785
+ receiptPath: result?.receiptPath ?? null,
1786
+ merge: input.fileTarget
1757
1787
  });
1758
1788
  return {
1759
1789
  captured,
@@ -1769,7 +1799,7 @@ function renderNextSteps(input) {
1769
1799
  if (backupWritten !== null && backupWritten.count > 0) report.log(` backup ${documentPath(cwd, backupDir(projectRoot))}/`);
1770
1800
  if (receiptPath || backupWritten !== null) report.log("");
1771
1801
  if (receiptPath) {
1772
- const verifyDir = documentPath(cwd, dirname(receiptPath));
1802
+ const verifyDir = documentPath(cwd, input.buildDir);
1773
1803
  const verifyCommand = verifyDir === "." ? "afterpack verify" : `afterpack verify ${verifyDir}`;
1774
1804
  report.log(alignRow("next", `${verifyCommand} before you deploy`));
1775
1805
  }
@@ -1812,6 +1842,12 @@ function refusalFor(error) {
1812
1842
  message: error.message,
1813
1843
  fix: error.fix
1814
1844
  };
1845
+ if (error instanceof AfterpackFilesError) return {
1846
+ exitCode: EXIT.failure,
1847
+ code: error.code,
1848
+ message: error.summary,
1849
+ fix: error.fix
1850
+ };
1815
1851
  if (error instanceof UnreadableReceiptError) return {
1816
1852
  exitCode: EXIT.failure,
1817
1853
  code: error.code,
@@ -1865,7 +1901,8 @@ function defaultCliStdout() {
1865
1901
  }
1866
1902
  async function run(deps) {
1867
1903
  const startedAt = Date.now();
1868
- const { cwd, engine, logger, version: version$1 } = deps;
1904
+ const { engine, logger, version: version$1 } = deps;
1905
+ const cwd = realDir(deps.cwd);
1869
1906
  const env = deps.env ?? process.env;
1870
1907
  const stdout = deps.stdout ?? defaultCliStdout();
1871
1908
  const argv = expandShortFlags(deps.argv);
@@ -1876,6 +1913,7 @@ async function run(deps) {
1876
1913
  const command = argv[0];
1877
1914
  if (command === "verify" || command === "audit" || command === "restore") return runSubcommand(command, argv.slice(1), {
1878
1915
  ...deps,
1916
+ cwd,
1879
1917
  env,
1880
1918
  stdout,
1881
1919
  argv
@@ -1960,7 +1998,6 @@ async function run(deps) {
1960
1998
  });
1961
1999
  if (resolved.positionals.length > 1) return refuseHere(EXIT.usage, "TOO_MANY_PATHS", `expected a single [path], got ${resolved.positionals.length}`, "Name one directory or one .js file; run it again for the second target.");
1962
2000
  const parsed = toRunOptions(resolved.config);
1963
- const projectRoot = projectRootFor(cwd, resolved.configFile);
1964
2001
  const backupEnabled = parsed.artifactOptions.build?.backup ?? true;
1965
2002
  const configuredKey = getPath(resolved.config, "key");
1966
2003
  const hasConfiguredKey = typeof configuredKey === "string" && configuredKey !== "" || typeof env.AFTERPACK_KEY === "string" && env.AFTERPACK_KEY !== "";
@@ -2010,10 +2047,18 @@ async function run(deps) {
2010
2047
  requested = join(explicitTarget, bare.requested);
2011
2048
  }
2012
2049
  }
2013
- const target = isAbsolute(requested) ? requested : resolve(cwd, requested);
2014
- if (!existsSync(target)) return refuseHere(EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Name an existing directory or .js file, or run `afterpack` with no path to detect the build output.");
2050
+ const requestedPath = isAbsolute(requested) ? requested : resolve(cwd, requested);
2051
+ if (!existsSync(requestedPath)) return refuseHere(EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Name an existing directory or .js file, or run `afterpack` with no path to detect the build output.");
2052
+ const target = realDir(requestedPath);
2015
2053
  const isDirectory = statSync(target).isDirectory();
2016
2054
  const buildDir = isDirectory ? target : dirname(target);
2055
+ let projectRoot;
2056
+ try {
2057
+ projectRoot = afterpackHome(buildDir, { fileTarget: !isDirectory }).projectRoot;
2058
+ } catch (error) {
2059
+ if (!(error instanceof AfterpackFilesError)) throw error;
2060
+ return refuseHere(EXIT.failure, error.code, error.summary, error.fix);
2061
+ }
2017
2062
  const nestedProjects = [];
2018
2063
  const files = collectJsFiles(target, {
2019
2064
  include: parsed.pathsInclude,
@@ -2040,6 +2085,8 @@ async function run(deps) {
2040
2085
  engine,
2041
2086
  cwd,
2042
2087
  buildDir,
2088
+ fileTarget: !isDirectory,
2089
+ sweepSourceMaps: isDirectory,
2043
2090
  sourceType: detectCliSourceType({
2044
2091
  files,
2045
2092
  buildDir
@@ -2096,6 +2143,7 @@ async function run(deps) {
2096
2143
  env,
2097
2144
  stdout,
2098
2145
  projectRoot,
2146
+ buildDir,
2099
2147
  receiptPath: result?.receiptPath,
2100
2148
  backupWritten,
2101
2149
  hasConfiguredKey
@@ -2148,6 +2196,7 @@ async function runSubcommand(command, rest, deps) {
2148
2196
  version: version$1
2149
2197
  });
2150
2198
  return audit({
2199
+ cwd,
2151
2200
  positionals: args.positionals,
2152
2201
  logger,
2153
2202
  report,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afterpack",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "AfterPack CLI: obfuscate the JavaScript in any build output, verify a build before deploy, and scan a live site",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,8 +14,8 @@
14
14
  "dist"
15
15
  ],
16
16
  "dependencies": {
17
- "@afterpack/core": "~0.2.4",
18
- "@afterpack/integration-utils": "0.2.3"
17
+ "@afterpack/core": "~0.2.5",
18
+ "@afterpack/integration-utils": "0.2.4"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@biomejs/biome": "2.4.4",