sealcode 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sealcode
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # sealcode
2
+
3
+ > Lock your source code inside your own git repo. Stop AI agents, scrapers, and curious eyes from reading what's yours.
4
+
5
+ ```bash
6
+ npm install -g sealcode
7
+ ```
8
+
9
+ > **Renamed from `vaultline`.** Existing vaults keep working unchanged — the on-disk format is unchanged, `.vaultlinerc.json` is still read, and `VAULTLINE_PASSPHRASE` is still accepted as a fallback.
10
+
11
+ ## What problem does this solve?
12
+
13
+ You push code to a private repo. You think it's safe. But anyone (or any AI) with read access to that repo — a contractor, a teammate's compromised laptop, a future Copilot indexing run, a leak — can read every line.
14
+
15
+ **sealcode** wraps your source files in AES-256-GCM ciphertext before they touch git. Your repo becomes a folder of opaque binary blobs with a stub `package.json`. To anyone without your passphrase, your codebase is uniformly random bytes.
16
+
17
+ To you, one command brings it all back:
18
+
19
+ ```bash
20
+ sealcode unlock # readable code is restored
21
+ # ... edit ...
22
+ sealcode lock # back to ciphertext
23
+ git commit && git push
24
+ ```
25
+
26
+ ## Quickstart (60 seconds)
27
+
28
+ ```bash
29
+ cd your-project
30
+ sealcode init # interactive wizard
31
+ # 1. Auto-detects your stack (Node, Python, Go, Rust, Ruby, PHP, Java, ...)
32
+ # 2. Picks safe include/exclude defaults
33
+ # 3. Asks for a passphrase
34
+ # 4. Shows a recovery code — write it down ONCE
35
+ # 5. Locks every source file immediately
36
+
37
+ git add . && git commit -m "vault initialized" && git push
38
+ ```
39
+
40
+ When you want to work on it:
41
+
42
+ ```bash
43
+ sealcode unlock
44
+ npm install
45
+ npm run dev
46
+ ```
47
+
48
+ When you're done:
49
+
50
+ ```bash
51
+ sealcode lock
52
+ git commit && git push
53
+ ```
54
+
55
+ ## Why this beats `git-crypt`, `git-secret`, `sops`, etc.
56
+
57
+ | | sealcode | git-crypt | git-secret | sops |
58
+ |---|---|---|---|---|
59
+ | Encrypts whole files (not just diffs) | ✅ | partial | ✅ | ❌ (values only) |
60
+ | Filenames hidden | ✅ | ❌ | ❌ | ❌ |
61
+ | Repo metadata hidden (package.json, deps) | ✅ | ❌ | ❌ | ❌ |
62
+ | Passphrase-based (no GPG keyring) | ✅ | ❌ | ❌ | partial |
63
+ | Recovery code if you lose passphrase | ✅ | ❌ | ❌ | ❌ |
64
+ | Works on any language/stack | ✅ | ✅ | ✅ | ✅ |
65
+ | Auto-detects your ecosystem | ✅ | ❌ | ❌ | ❌ |
66
+ | Built-in drift detection | ✅ | ❌ | ❌ | ❌ |
67
+
68
+ The big one: **sealcode hides that your code exists at all**. Other tools encrypt the files but leave the folder structure, the filenames, and the `package.json` (with every telltale dependency) in plain view.
69
+
70
+ ## Commands
71
+
72
+ ```
73
+ sealcode init # one-time setup; interactive wizard
74
+ sealcode lock # encrypt source → vendor/ blobs (removes plaintext)
75
+ sealcode unlock # decrypt blobs → restore source (removes stubs)
76
+ sealcode verify # confirm every blob decrypts & matches its hash
77
+ sealcode status # show locked/unlocked + drift since last lock
78
+ sealcode status --check # exit 1 if unlocked with drift (used by git hook)
79
+ sealcode status --json # machine-readable state (editors / scripts)
80
+ sealcode rotate # change passphrase (blobs unchanged); env: SEALCODE_OLD_PASSPHRASE / SEALCODE_NEW_PASSPHRASE
81
+ sealcode backup <dir> # copy locked vault + config snapshot to a new folder
82
+ sealcode restore <dir> # restore from backup (use --force if locked dir exists)
83
+ sealcode install-hook # git pre-commit: block commits when unlocked + drift
84
+ sealcode uninstall-hook # remove hook block
85
+ sealcode panic # immediate re-lock + session wipe
86
+ sealcode logout # clear cached session, force passphrase next time
87
+ sealcode presets # list supported ecosystems
88
+ sealcode pro # Pro tier info
89
+ sealcode where # debug: print paths and state (no secrets)
90
+ ```
91
+
92
+ Aliases: `sc`, `sealcode seal` = `lock`, `sealcode open` = `unlock`.
93
+
94
+ ### Editor support
95
+
96
+ A VS Code companion lives in `tools/vaultline-vscode/` — it shows the current lock state in the status bar when the CLI is on your `PATH`. (Rename in flight; the extension itself works against either `sealcode` or `vaultline` on `PATH`.)
97
+
98
+ ### CI (GitHub Actions)
99
+
100
+ ```yaml
101
+ - run: npm install -g sealcode
102
+ - env:
103
+ SEALCODE_PASSPHRASE: ${{ secrets.SEALCODE_PASSPHRASE }}
104
+ run: |
105
+ sealcode unlock
106
+ npm install --omit=dev
107
+ npm run build
108
+ ```
109
+
110
+ ## Supported stacks (auto-detected)
111
+
112
+ | Marker file | Preset | Locked dir |
113
+ |---|---|---|
114
+ | `package.json` (no Next config) | Node.js / TypeScript | `vendor/` |
115
+ | `next.config.*` | Next.js | `vendor/` |
116
+ | `requirements.txt` / `pyproject.toml` / `setup.py` | Python | `_site_packages/` |
117
+ | `manage.py` | Django | `_site_packages/` |
118
+ | `go.mod` | Go | `internal/sealed/` |
119
+ | `Cargo.toml` | Rust | `target/.cache/` |
120
+ | `Gemfile` | Ruby on Rails | `vendor/sealed/` |
121
+ | `composer.json` | PHP / Laravel | `storage/sealed/` |
122
+ | `pom.xml` / `build.gradle*` | Java / JVM | `.sealed/` |
123
+ | (none of the above) | generic | `vendor/` |
124
+
125
+ Each preset uses a `lockedDir` name that looks native to its ecosystem — Go projects don't get a suspicious folder called `locked/`, they get `internal/sealed/`. Stealth by camouflage.
126
+
127
+ ## How the crypto works (the short version)
128
+
129
+ - **Algorithm:** AES-256-GCM with a random 12-byte IV per blob; gzip before encrypt.
130
+ - **Key derivation:** scrypt(passphrase, salt) → 32-byte wrapping key, N=2^17, r=8, p=1 (~1s, ~128MB).
131
+ - **Master data key:** randomly generated, stored on disk wrapped twice — once by your passphrase, once by a 128-bit recovery seed (the 30-char recovery code you wrote down at init).
132
+ - **File-on-disk format:** `[12B IV][16B GCM tag][ciphertext]` with no header bytes. Indistinguishable from random data.
133
+ - **Filenames inside `vendor/`:** HMAC-SHA256 of the path, sharded by first 2 hex chars, truncated to 12. Stable across re-locks, indistinguishable across files.
134
+ - **No `# CV1` or other text signatures** that would let `file(1)` / scrapers / AI identify the format.
135
+
136
+ ## Where does the passphrase live?
137
+
138
+ Wherever you put it. The tool checks:
139
+
140
+ 1. Cached session (~8h after last unlock — stored at `~/.sealcode/sessions/<id>`, encrypted with a host-binding key)
141
+ 2. `SEALCODE_PASSPHRASE` env var (use in CI). `VAULTLINE_PASSPHRASE` is accepted as a fallback for projects upgrading from vaultline 1.x.
142
+ 3. Interactive prompt
143
+
144
+ **sealcode never writes your passphrase to disk in any recoverable form.** Lose your passphrase AND your recovery code, and your code is gone. We can't help you. That's not a bug — it's the entire point.
145
+
146
+ Back up your recovery code in:
147
+ - a password manager (1Password / Bitwarden as a secure note)
148
+ - a printed sheet
149
+ - an encrypted USB / hardware key
150
+
151
+ ## CI / deploy
152
+
153
+ ```yaml
154
+ # GitHub Actions
155
+ - run: npm install -g sealcode
156
+ - env:
157
+ SEALCODE_PASSPHRASE: ${{ secrets.SEALCODE_PASSPHRASE }}
158
+ run: |
159
+ sealcode unlock
160
+ npm install --omit=dev
161
+ npm run build # produces dist/main.js
162
+ - run: rsync dist/ user@server:/app/dist/
163
+ ```
164
+
165
+ Your production server never sees the passphrase or the vault. It only runs the built `dist/`.
166
+
167
+ ## Pro features (coming soon)
168
+
169
+ The free CLI handles lock / unlock / verify forever. **sealcode Pro** adds:
170
+
171
+ - **Team key sharing** — invite teammates, no passphrase exchange needed
172
+ - **Key rotation across N projects** with one command
173
+ - **Audit log** — see who unlocked what, when
174
+ - **Cloud key escrow** — passphrase recovery via account auth
175
+ - **Temporary access codes** — time-boxed, revocable read access to a project
176
+ - **CI/CD tokens** — short-lived deploy keys, no long-lived secrets in Actions
177
+
178
+ Start a 14-day trial at [sealcode.dev/pro](https://sealcode.dev/pro).
179
+
180
+ ## FAQ
181
+
182
+ **Does this affect git diffs?**
183
+ Yes — `vendor/` blobs change every lock (random IV). For human review, diff your *local* unlocked tree before locking. A `sealcode diff <commit>` command is on the Pro roadmap.
184
+
185
+ **Can I encrypt only some files?**
186
+ Yes — edit `.sealcoderc.json`'s `include` array.
187
+
188
+ **Does it work on Windows?**
189
+ Yes. Forward-slash normalization in opaque names; tested on macOS, Linux, Windows.
190
+
191
+ **What's the perf cost?**
192
+ First unlock with passphrase: ~1s (scrypt). Subsequent commands within 8h: <100ms (cached session). Lock/unlock of a 1000-file repo: ~2s.
193
+
194
+ **Is this audited?**
195
+ Not formally. The crypto is standard primitives (AES-256-GCM, scrypt) used in their textbook configurations. Audit yourself at the repo. We'll fund a formal audit once revenue supports it.
196
+
197
+ **I had `vaultline` installed before — do I lose my vault?**
198
+ No. The on-disk format is unchanged. `sealcode` reads any existing `.vaultlinerc.json`, your old `~/.vaultline/sessions/` cache, and `VAULTLINE_PASSPHRASE` env var. On the next `sealcode lock` the project will start writing the new config name (`.sealcoderc.json`), but the encrypted blobs themselves never need to be re-encrypted.
199
+
200
+ ## License
201
+
202
+ MIT. See [LICENSE](./LICENSE).
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { run } = require('../src/cli');
5
+
6
+ run(process.argv).catch((err) => {
7
+ // Last-resort error path; cli.js handles known errors with friendly output.
8
+ process.stderr.write((err && err.stack) || String(err));
9
+ process.stderr.write('\n');
10
+ process.exit(1);
11
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "sealcode",
3
+ "version": "0.1.0",
4
+ "description": "Lock your source code in your own git repo. Stop AI agents, scrapers, and curious eyes from reading what's yours.",
5
+ "keywords": [
6
+ "encryption",
7
+ "source-code",
8
+ "privacy",
9
+ "git",
10
+ "ai-privacy",
11
+ "obfuscation",
12
+ "vault",
13
+ "secrets",
14
+ "code-protection",
15
+ "intellectual-property",
16
+ "encrypt",
17
+ "lock",
18
+ "seal",
19
+ "sealcode"
20
+ ],
21
+ "homepage": "https://sealcode.dev",
22
+ "bugs": {
23
+ "url": "https://github.com/sealcode/sealcode/issues"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/sealcode/sealcode.git"
28
+ },
29
+ "license": "MIT",
30
+ "author": "sealcode",
31
+ "main": "src/index.js",
32
+ "bin": {
33
+ "sealcode": "bin/sealcode.js",
34
+ "sc": "bin/sealcode.js"
35
+ },
36
+ "files": [
37
+ "bin",
38
+ "src",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "test": "node test/roundtrip.js",
44
+ "selftest": "node test/roundtrip.js"
45
+ },
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "dependencies": {
50
+ "commander": "^12.1.0",
51
+ "fast-glob": "^3.3.2"
52
+ }
53
+ }
package/src/api.js ADDED
@@ -0,0 +1,162 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Sealcode CLI ↔ web app glue.
5
+ *
6
+ * - Credentials live at ~/.sealcode/credentials.json (mode 0600).
7
+ * - Bearer token is sent on every authenticated request.
8
+ * - `apiUrl` is read from credentials, then SEALCODE_API_URL, then a default.
9
+ *
10
+ * The credentials file is intentionally minimal: it stores who is logged in
11
+ * (so `sealcode whoami` works offline) and the long-lived token. We never
12
+ * cache project lists or grants there.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+ const pkg = require('../package.json');
19
+
20
+ const CONFIG_DIR = path.join(os.homedir(), '.sealcode');
21
+ const CREDS_PATH = path.join(CONFIG_DIR, 'credentials.json');
22
+ const DEFAULT_API_URL = 'https://sealcode.dev';
23
+
24
+ function defaultApiUrl() {
25
+ return process.env.SEALCODE_API_URL || DEFAULT_API_URL;
26
+ }
27
+
28
+ function readCreds() {
29
+ try {
30
+ if (!fs.existsSync(CREDS_PATH)) return null;
31
+ const raw = fs.readFileSync(CREDS_PATH, 'utf8');
32
+ const parsed = JSON.parse(raw);
33
+ if (!parsed || typeof parsed !== 'object') return null;
34
+ return parsed;
35
+ } catch (_) {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ function writeCreds(creds) {
41
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
42
+ // Write to a temp file then rename so a partial write can't leave a corrupt
43
+ // credentials file behind.
44
+ const tmp = CREDS_PATH + '.tmp';
45
+ fs.writeFileSync(tmp, JSON.stringify(creds, null, 2) + '\n', { mode: 0o600 });
46
+ try {
47
+ fs.chmodSync(tmp, 0o600);
48
+ } catch (_) { /* ignore on Windows */ }
49
+ fs.renameSync(tmp, CREDS_PATH);
50
+ }
51
+
52
+ function clearCreds() {
53
+ try {
54
+ fs.unlinkSync(CREDS_PATH);
55
+ } catch (_) { /* ignore */ }
56
+ }
57
+
58
+ function getApiUrl() {
59
+ // Env var wins so test setups can point at a local dev server without
60
+ // editing credentials.
61
+ if (process.env.SEALCODE_API_URL) return process.env.SEALCODE_API_URL;
62
+ const c = readCreds();
63
+ return (c && c.apiUrl) || DEFAULT_API_URL;
64
+ }
65
+
66
+ function getToken() {
67
+ if (process.env.SEALCODE_TOKEN) return process.env.SEALCODE_TOKEN;
68
+ const c = readCreds();
69
+ return c && c.token ? c.token : null;
70
+ }
71
+
72
+ function clientInfo() {
73
+ return {
74
+ hostname: os.hostname(),
75
+ platform: `${os.platform()}-${os.arch()}`,
76
+ cliVersion: pkg.version,
77
+ };
78
+ }
79
+
80
+ class ApiError extends Error {
81
+ constructor(status, code, message, raw) {
82
+ super(message);
83
+ this.status = status;
84
+ this.apiCode = code;
85
+ this.raw = raw;
86
+ }
87
+ }
88
+
89
+ async function request(method, urlPath, { body, auth = false, apiUrl } = {}) {
90
+ const base = apiUrl || getApiUrl();
91
+ const url = new URL(urlPath, base).toString();
92
+ const headers = {
93
+ 'content-type': 'application/json',
94
+ 'user-agent': `sealcode/${pkg.version}`,
95
+ accept: 'application/json',
96
+ };
97
+ if (auth) {
98
+ const token = getToken();
99
+ if (!token) {
100
+ throw new ApiError(
101
+ 401,
102
+ 'unauthenticated',
103
+ 'You are not logged in. Run `sealcode login` first.',
104
+ );
105
+ }
106
+ headers.authorization = `Bearer ${token}`;
107
+ }
108
+
109
+ let res;
110
+ try {
111
+ res = await fetch(url, {
112
+ method,
113
+ headers,
114
+ body: body == null ? undefined : JSON.stringify(body),
115
+ });
116
+ } catch (err) {
117
+ throw new ApiError(
118
+ 0,
119
+ 'network',
120
+ `Could not reach ${base}: ${err.message || err}.`,
121
+ );
122
+ }
123
+
124
+ const ct = res.headers.get('content-type') || '';
125
+ let parsed = null;
126
+ if (ct.includes('application/json')) {
127
+ try {
128
+ parsed = await res.json();
129
+ } catch {
130
+ parsed = null;
131
+ }
132
+ } else {
133
+ try {
134
+ parsed = { text: await res.text() };
135
+ } catch {
136
+ parsed = null;
137
+ }
138
+ }
139
+
140
+ if (!res.ok) {
141
+ const code = (parsed && parsed.error && parsed.error.code) || 'http_error';
142
+ const message =
143
+ (parsed && parsed.error && parsed.error.message) ||
144
+ `${method} ${urlPath} → HTTP ${res.status}`;
145
+ throw new ApiError(res.status, code, message, parsed);
146
+ }
147
+ return parsed || {};
148
+ }
149
+
150
+ module.exports = {
151
+ ApiError,
152
+ CONFIG_DIR,
153
+ CREDS_PATH,
154
+ defaultApiUrl,
155
+ readCreds,
156
+ writeCreds,
157
+ clearCreds,
158
+ getApiUrl,
159
+ getToken,
160
+ clientInfo,
161
+ request,
162
+ };
package/src/bundle.js ADDED
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Portable backup of a sealcode project's locked directory + config snapshot.
5
+ *
6
+ * `export` writes a directory:
7
+ * <dest>/
8
+ * manifest.json — metadata (sealcode version, lockedDir, paths)
9
+ * locked/ — recursive copy of the locked dir on disk
10
+ *
11
+ * `import` copies `locked/` from a bundle back into the project and optionally
12
+ * restores `.sealcoderc.json` if present in the bundle.
13
+ *
14
+ * Backward-compat: bundles produced by vaultline 1.x carry a
15
+ * `vaultlinerc.json.bak` snapshot under the same scheme — we read that too
16
+ * and restore it as `.sealcoderc.json` on import.
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const pkg = require('../package.json');
22
+ const { CONFIG_FILE, LEGACY_CONFIG_FILE } = require('./config');
23
+
24
+ const CONFIG_BAK_NAME = 'sealcoderc.json.bak';
25
+ const LEGACY_CONFIG_BAK_NAME = 'vaultlinerc.json.bak';
26
+
27
+ function copyDirRecursive(src, dest) {
28
+ fs.mkdirSync(dest, { recursive: true });
29
+ for (const name of fs.readdirSync(src)) {
30
+ const s = path.join(src, name);
31
+ const d = path.join(dest, name);
32
+ const st = fs.statSync(s);
33
+ if (st.isDirectory()) copyDirRecursive(s, d);
34
+ else fs.copyFileSync(s, d);
35
+ }
36
+ }
37
+
38
+ function rmDir(dir) {
39
+ fs.rmSync(dir, { recursive: true, force: true });
40
+ }
41
+
42
+ /**
43
+ * @param {string} projectRoot
44
+ * @param {object} config — active config with lockedDir
45
+ * @param {string} dest — path to output folder (created)
46
+ */
47
+ async function exportBundle(projectRoot, config, dest) {
48
+ const absDest = path.resolve(dest);
49
+ const lockedDir = config.lockedDir;
50
+ const lockedSrc = path.join(projectRoot, lockedDir);
51
+ if (!fs.existsSync(lockedSrc)) {
52
+ throw new Error(`sealcode: locked directory not found: ${lockedSrc}`);
53
+ }
54
+ if (fs.existsSync(absDest)) {
55
+ throw new Error(`sealcode: destination already exists: ${absDest}`);
56
+ }
57
+
58
+ fs.mkdirSync(absDest, { recursive: true });
59
+ const bundleLocked = path.join(absDest, 'locked');
60
+ copyDirRecursive(lockedSrc, bundleLocked);
61
+
62
+ // Prefer the new config; fall back to a legacy file from a not-yet-migrated
63
+ // project so a backup taken right after install still captures the config.
64
+ let cfgPath = path.join(projectRoot, CONFIG_FILE);
65
+ if (!fs.existsSync(cfgPath)) cfgPath = path.join(projectRoot, LEGACY_CONFIG_FILE);
66
+
67
+ let configSnapshot = null;
68
+ if (fs.existsSync(cfgPath)) {
69
+ configSnapshot = fs.readFileSync(cfgPath, 'utf8');
70
+ }
71
+
72
+ const meta = {
73
+ v: 1,
74
+ sealcode: pkg.version,
75
+ lockedDir,
76
+ createdAt: new Date().toISOString(),
77
+ hasSealcoderc: configSnapshot != null,
78
+ };
79
+ fs.writeFileSync(path.join(absDest, 'manifest.json'), JSON.stringify(meta, null, 2) + '\n', 'utf8');
80
+ if (configSnapshot) {
81
+ fs.writeFileSync(path.join(absDest, CONFIG_BAK_NAME), configSnapshot, 'utf8');
82
+ }
83
+
84
+ return { path: absDest, meta };
85
+ }
86
+
87
+ /**
88
+ * @param {string} projectRoot
89
+ * @param {string} bundlePath — folder produced by exportBundle
90
+ * @param {{ force?: boolean }} opts
91
+ */
92
+ async function importBundle(projectRoot, bundlePath, opts = {}) {
93
+ const abs = path.resolve(bundlePath);
94
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) {
95
+ throw new Error(`sealcode: bundle not found: ${abs}`);
96
+ }
97
+ const manPath = path.join(abs, 'manifest.json');
98
+ if (!fs.existsSync(manPath)) {
99
+ throw new Error('sealcode: bundle missing manifest.json');
100
+ }
101
+ const meta = JSON.parse(fs.readFileSync(manPath, 'utf8'));
102
+ const lockedDir = meta.lockedDir;
103
+ if (!lockedDir || typeof lockedDir !== 'string') {
104
+ throw new Error('sealcode: bundle manifest missing lockedDir');
105
+ }
106
+ const bundleLocked = path.join(abs, 'locked');
107
+ if (!fs.existsSync(bundleLocked)) {
108
+ throw new Error('sealcode: bundle missing locked/ directory');
109
+ }
110
+
111
+ const destLocked = path.join(projectRoot, lockedDir);
112
+ if (fs.existsSync(destLocked)) {
113
+ if (!opts.force) {
114
+ throw new Error(
115
+ `sealcode: ${destLocked} already exists. Pass --force to replace (DANGER).`
116
+ );
117
+ }
118
+ rmDir(destLocked);
119
+ }
120
+ copyDirRecursive(bundleLocked, destLocked);
121
+
122
+ // Prefer the new bak name; fall back to the legacy one. Either way we
123
+ // restore as the new .sealcoderc.json on the project side.
124
+ let bak = path.join(abs, CONFIG_BAK_NAME);
125
+ if (!fs.existsSync(bak)) bak = path.join(abs, LEGACY_CONFIG_BAK_NAME);
126
+ if (fs.existsSync(bak)) {
127
+ fs.copyFileSync(bak, path.join(projectRoot, CONFIG_FILE));
128
+ }
129
+
130
+ return { lockedDir, meta };
131
+ }
132
+
133
+ module.exports = { exportBundle, importBundle };