codexport 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  ---
10
10
 
11
- `codexport` replicates a canonical Machine1 Codex setup to follower machines. it is built for operators who want one trusted `~/.codex` source of truth, follower-local overlays, and a low-friction `npx` join path without committing plaintext secrets to GitHub.
11
+ `codexport` replicates a canonical master Codex setup to follower machines. it is built for operators who want one trusted `~/.codex` source of truth, follower-local overlays, and a low-friction `npx` join path without committing plaintext secrets to GitHub.
12
12
 
13
- Machine1 serves a content-hashed bundle from its `~/.codex` directory. followers pin the master's fingerprint on join, fetch updates over a Tailscale-reachable HTTP address, and apply updates at Codex `SessionStart` through a short best-effort hook.
13
+ the master serves a content-hashed bundle from its `~/.codex` directory. followers pin the master's fingerprint on join, fetch updates over a Tailscale-reachable HTTP address, and apply updates at Codex `SessionStart` through a short best-effort hook.
14
14
 
15
15
  [npm](https://www.npmjs.com/package/codexport) | [github](https://github.com/Microck/codexport)
16
16
 
@@ -18,7 +18,7 @@ Machine1 serves a content-hashed bundle from its `~/.codex` directory. followers
18
18
 
19
19
  if you keep a carefully tuned Codex setup on one machine and want the same defaults elsewhere, `codexport` gives you a practical pull-based sync path.
20
20
 
21
- - keep Machine1 as the canonical Codex configuration source
21
+ - keep the master as the canonical Codex configuration source
22
22
  - let followers preserve local MCPs, local skills, trust entries, and path overrides
23
23
  - sync auth-bearing files through the private Tailscale path instead of a plaintext GitHub commit
24
24
  - refresh followers at Codex session startup without interrupting active sessions
@@ -28,18 +28,18 @@ if you keep a carefully tuned Codex setup on one machine and want the same defau
28
28
 
29
29
  `codexport` requires Node.js 20+.
30
30
 
31
- on Machine1:
31
+ on the master:
32
32
 
33
33
  ```bash
34
34
  npx codexport master init
35
35
  npx codexport master service install
36
- npx codexport master link --host machine1.tailnet.ts.net
36
+ npx codexport master link --host master.tailnet.ts.net
37
37
  ```
38
38
 
39
39
  on a follower:
40
40
 
41
41
  ```bash
42
- npx codexport follower join "codexport://join?host=machine1.tailnet.ts.net&port=17342&fingerprint=..."
42
+ npx codexport follower join "codexport://join?host=master.tailnet.ts.net&port=17342&fingerprint=..."
43
43
  npx codexport hook install
44
44
  ```
45
45
 
@@ -52,16 +52,58 @@ npx codexport status
52
52
 
53
53
  ## sync model
54
54
 
55
- ```text
56
- Machine1 ~/.codex
57
- -> codexport master serve
58
- -> Tailscale-reachable HTTP bundle
59
- -> follower sync/apply
60
- -> generated follower ~/.codex
55
+ ```mermaid
56
+ flowchart LR
57
+ subgraph master["master machine"]
58
+ masterCodex["~/.codex canonical state"]
59
+ masterCli["codexport master serve"]
60
+ masterBundle["content-hashed bundle"]
61
+ end
62
+
63
+ subgraph privateNet["tailscale network"]
64
+ http["http://master.tailnet.ts.net:17342"]
65
+ end
66
+
67
+ subgraph follower["follower machine"]
68
+ localOverlay["~/.codexport local overlay"]
69
+ sessionHook["Codex SessionStart hook"]
70
+ generatedCodex["generated ~/.codex"]
71
+ end
72
+
73
+ masterCodex -->|select files and hash content| masterBundle
74
+ masterBundle --> masterCli
75
+ masterCli -->|serve bundle and fingerprint| http
76
+ sessionHook -->|check revision before session| http
77
+ http -->|download changed bundle| sessionHook
78
+ localOverlay -->|merge MCPs, skills, path variables| sessionHook
79
+ sessionHook -->|backup and apply| generatedCodex
61
80
  ```
62
81
 
63
82
  followers trust the provided Tailscale address and store the master fingerprint. later syncs refuse changed fingerprints by default, so a changed master identity requires intentional re-enrollment.
64
83
 
84
+ ## trust flow
85
+
86
+ ```mermaid
87
+ sequenceDiagram
88
+ participant Operator as operator
89
+ participant Master as master
90
+ participant Follower as follower
91
+ participant Codex as codex session
92
+
93
+ Operator->>Master: codexport master link
94
+ Master-->>Operator: join link with host, port, fingerprint
95
+ Operator->>Follower: codexport follower join "codexport://join?..."
96
+ Follower->>Master: GET /meta
97
+ Master-->>Follower: fingerprint and revision
98
+ Follower->>Follower: pin trusted fingerprint
99
+ Follower->>Master: GET /bundle
100
+ Master-->>Follower: content-hashed bundle
101
+ Follower->>Follower: apply bundle plus local overlay
102
+ Codex->>Follower: SessionStart hook
103
+ Follower->>Master: check revision and fingerprint
104
+ Follower-->>Codex: continue with latest applied config
105
+ ```
106
+
65
107
  ## included state
66
108
 
67
109
  the master bundle includes canonical Codex config, auth files, hooks, prompts,
@@ -100,7 +142,7 @@ the follower's `local.toml` before writing the generated `~/.codex/config.toml`.
100
142
 
101
143
  | command | purpose |
102
144
  | --- | --- |
103
- | `codexport master init` | create or refresh Machine1 master identity and bundle state |
145
+ | `codexport master init` | create or refresh the master identity and bundle state |
104
146
  | `codexport master serve` | serve the current canonical bundle over HTTP |
105
147
  | `codexport master link` | print a durable follower join link and fallback command |
106
148
  | `codexport master rebuild` | force rebuild the master bundle for repair/debugging |
@@ -125,14 +167,14 @@ followers do not need a background service in v1. the hook runs a short best-eff
125
167
  generate a copy-paste join command:
126
168
 
127
169
  ```bash
128
- codexport master link --host machine1.tailnet.ts.net
170
+ codexport master link --host master.tailnet.ts.net
129
171
  ```
130
172
 
131
173
  join with explicit trust metadata:
132
174
 
133
175
  ```bash
134
176
  codexport follower join \
135
- --master http://machine1.tailnet.ts.net:17342 \
177
+ --master http://master.tailnet.ts.net:17342 \
136
178
  --fingerprint <fingerprint> \
137
179
  --apply
138
180
  ```
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { Command, Option } from "commander";
3
3
  import chokidar from "chokidar";
4
4
  import { createHash, randomBytes } from "node:crypto";
5
5
  import { createServer, request } from "node:http";
6
- import { mkdir, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
6
+ import { lstat, mkdir, readFile, readdir, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
7
7
  import { existsSync } from "node:fs";
8
8
  import { realpathSync } from "node:fs";
9
9
  import { fileURLToPath } from "node:url";
@@ -11,7 +11,7 @@ import { homedir, platform } from "node:os";
11
11
  import path from "node:path";
12
12
  import { spawn } from "node:child_process";
13
13
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
14
- const VERSION = "0.1.1";
14
+ const VERSION = "0.1.3";
15
15
  const DEFAULT_PORT = 17342;
16
16
  const DEFAULT_TIMEOUT_MS = 5_000;
17
17
  const CODEXPORT_DIR = ".codexport";
@@ -187,7 +187,10 @@ async function walkIncluded(root, absolute, files) {
187
187
  const relative = normalizeRelative(path.relative(root, absolute));
188
188
  if (!relative || shouldExclude(relative))
189
189
  return;
190
- const entryStat = await stat(absolute);
190
+ const entryStat = await lstat(absolute);
191
+ if (entryStat.isSymbolicLink()) {
192
+ return;
193
+ }
191
194
  if (entryStat.isDirectory()) {
192
195
  const children = await readdir(absolute);
193
196
  for (const child of children) {
@@ -533,7 +536,7 @@ async function commandMasterLink(ctx, options) {
533
536
  const identity = await loadMasterIdentity(ctx);
534
537
  const local = await readLocalConfig(ctx);
535
538
  const port = options.port ?? local.port ?? DEFAULT_PORT;
536
- const host = options.host ?? "machine1.tailnet.ts.net";
539
+ const host = options.host ?? "master.tailnet.ts.net";
537
540
  const url = masterUrl(host, port);
538
541
  print(ctx, {
539
542
  joinLink: buildJoinLink(url, identity.fingerprint),
@@ -674,7 +677,7 @@ async function main(argv) {
674
677
  .name("codexport")
675
678
  .description("Replicate a canonical Codex setup from a master machine to follower machines.")
676
679
  .version(VERSION);
677
- const master = program.command("master").description("Manage the canonical Machine1 export.");
680
+ const master = program.command("master").description("Manage the canonical master export.");
678
681
  master.command("init")
679
682
  .description("Create or refresh master identity and canonical bundle state.")
680
683
  .option("--port <port>", "default serve port", parsePositiveInt, DEFAULT_PORT)
@@ -684,7 +687,7 @@ async function main(argv) {
684
687
  .action(async (_options, command) => commandMasterRebuild(contextFromCommand(command)));
685
688
  master.command("link")
686
689
  .description("Print a durable follower join link and copy-paste command.")
687
- .option("--host <host>", "Tailscale host, IP, or full URL", "machine1.tailnet.ts.net")
690
+ .option("--host <host>", "Tailscale host, IP, or full URL", "master.tailnet.ts.net")
688
691
  .option("--port <port>", "master port", parsePositiveInt, DEFAULT_PORT)
689
692
  .action(async (options, command) => commandMasterLink(contextFromCommand(command), options));
690
693
  master.command("serve")
@@ -706,7 +709,7 @@ async function main(argv) {
706
709
  const follower = program.command("follower").description("Enroll and manage a follower machine.");
707
710
  follower.command("join [link]")
708
711
  .description("Enroll this follower from a codexport://join link or explicit master URL.")
709
- .option("--master <url>", "master URL, for example http://machine1.tailnet.ts.net:17342")
712
+ .option("--master <url>", "master URL, for example http://master.tailnet.ts.net:17342")
710
713
  .option("--fingerprint <hex>", "expected master fingerprint")
711
714
  .option("--apply", "download and apply immediately after enrollment")
712
715
  .option("--timeout-ms <ms>", "network timeout", parsePositiveInt, DEFAULT_TIMEOUT_MS)
package/docs/prd.md CHANGED
@@ -7,14 +7,14 @@ Status: implemented as an initial Node.js CLI in this repository.
7
7
  Build a low-friction way to replicate the useful parts of a master machine's
8
8
  Codex setup to follower machines.
9
9
 
10
- The main target is `~/.codex`. Machine1 is the source of truth. Follower
10
+ The main target is `~/.codex`. The master is the source of truth. Follower
11
11
  machines should stay up to date automatically while still being allowed to keep
12
12
  machine-local additions such as local MCPs, local skills, local trust entries,
13
13
  and local path overrides.
14
14
 
15
15
  ## Product Principles
16
16
 
17
- - Machine1 owns the canonical Codex configuration.
17
+ - The master owns the canonical Codex configuration.
18
18
  - Followers are read-only consumers of canonical state.
19
19
  - Followers can keep local-only overlays unless those overlays explicitly
20
20
  conflict with canonical names.
@@ -37,32 +37,32 @@ The project will live at:
37
37
 
38
38
  ### Authority Model
39
39
 
40
- Machine1 is canonical. Follower machines do not push changes back by default.
40
+ The master is canonical. Follower machines do not push changes back by default.
41
41
 
42
42
  Follower-specific additions are allowed, but they remain local unless promoted
43
- from Machine1 intentionally.
43
+ from the master intentionally.
44
44
 
45
45
  ### Sync Transport
46
46
 
47
47
  Use a Tailscale-compatible pull model:
48
48
 
49
49
  ```text
50
- Machine1:
50
+ Master:
51
51
  codexport serve
52
52
 
53
53
  Follower:
54
54
  codexport join
55
- > Master Tailscale IP/name: machine1.tailnet.ts.net
55
+ > Master Tailscale IP/name: master.tailnet.ts.net
56
56
  ```
57
57
 
58
- Followers pull from Machine1. Machine1 does not need to track and push to every
58
+ Followers pull from the master. The master does not need to track and push to every
59
59
  follower.
60
60
 
61
61
  Tailscale reachability is sufficient for follower enrollment. The first version
62
62
  does not require a separate one-time pairing code.
63
63
 
64
- Machine1 should run the master server persistently as a user-level service.
65
- Followers should be able to reconnect and sync whenever Machine1 is online.
64
+ The master should run the master server persistently as a user-level service.
65
+ Followers should be able to reconnect and sync whenever the master is online.
66
66
 
67
67
  On first join, the follower trusts the provided Tailscale address and stores the
68
68
  master instance fingerprint. Later syncs must verify that fingerprint. If the
@@ -71,7 +71,7 @@ re-enroll or trust-reset command.
71
71
 
72
72
  ### One-Click Join
73
73
 
74
- Machine1 should be able to generate a durable join artifact for followers.
74
+ The master should be able to generate a durable join artifact for followers.
75
75
 
76
76
  Preferred UX:
77
77
 
@@ -83,14 +83,14 @@ Outputs a permanent join link or command that a follower can run without
83
83
  manually typing the master address:
84
84
 
85
85
  ```text
86
- npx codexport follower join "codexport://join?host=machine1.tailnet.ts.net&port=17342&fingerprint=..."
86
+ npx codexport follower join "codexport://join?host=master.tailnet.ts.net&port=17342&fingerprint=..."
87
87
  ```
88
88
 
89
89
  If custom URL handling is too complex for the first implementation, the
90
90
  fallback is a one-time copy-paste command:
91
91
 
92
92
  ```text
93
- npx codexport follower join --master http://machine1.tailnet.ts.net:17342 --fingerprint ...
93
+ npx codexport follower join --master http://master.tailnet.ts.net:17342 --fingerprint ...
94
94
  ```
95
95
 
96
96
  The join link should not include plaintext Codex secrets. It should include only
@@ -128,9 +128,9 @@ Followers should sync automatically through a follower-only Codex `SessionStart`
128
128
  hook:
129
129
 
130
130
  - The hook runs a short best-effort sync before a new Codex session starts.
131
- - If Machine1 is reachable and the content hash changed, the follower applies
131
+ - If the master is reachable and the content hash changed, the follower applies
132
132
  the update before the session continues.
133
- - If Machine1 is unavailable, the hook exits cleanly and Codex starts with the
133
+ - If the master is unavailable, the hook exits cleanly and Codex starts with the
134
134
  most recently applied config.
135
135
 
136
136
  This means followers are guaranteed to refresh at the Codex session boundary,
@@ -142,7 +142,7 @@ available when an immediate refresh is needed outside session startup.
142
142
  The tool should generate the final `~/.codex/config.toml` from layers:
143
143
 
144
144
  ```text
145
- canonical config from Machine1
145
+ canonical config from the master
146
146
  local follower overlay
147
147
  generated final ~/.codex/config.toml
148
148
  ```
@@ -213,7 +213,7 @@ The user wants auth and important state to sync for a one-click experience.
213
213
 
214
214
  The current design should not store plaintext secrets in GitHub. Instead:
215
215
 
216
- - Machine1 can include secret-bearing files in a private export bundle served
216
+ - The master can include secret-bearing files in a private export bundle served
217
217
  over Tailscale.
218
218
  - Followers fetch and apply that bundle during `join` or automatic sync.
219
219
  - GitHub stores code and non-secret portable config, not plaintext token blobs.
@@ -265,7 +265,7 @@ Recommended package shape:
265
265
  - Optional later installer that writes the master service and follower hook.
266
266
 
267
267
  The tool may still install or configure Bun as part of the synced development
268
- toolchain if Machine1's `mise.toml` requests it.
268
+ toolchain if the master's `mise.toml` requests it.
269
269
 
270
270
  The published package should require Node.js 20 or newer.
271
271
 
@@ -307,10 +307,10 @@ Rejected for v1:
307
307
 
308
308
  Expected roles:
309
309
 
310
- - `master init`: create Machine1 canonical state.
310
+ - `master init`: create the master canonical state.
311
311
  - `master serve`: serve canonical bundle over Tailscale-reachable HTTP.
312
312
  - `master link`: print a durable follower join link or copy-paste command.
313
- - `follower join`: enroll a follower by asking for Machine1 Tailscale IP/name.
313
+ - `follower join`: enroll a follower by asking for the master Tailscale IP/name.
314
314
  - `sync`: fetch and stage/apply updates.
315
315
  - `apply`: apply already available canonical state plus local overlays.
316
316
  - `hook install`: install follower-only Codex SessionStart sync hook.
@@ -352,7 +352,7 @@ Follower hook:
352
352
  - Downloads and validates new revisions when changed.
353
353
  - Verifies the stored master fingerprint before accepting an update.
354
354
  - Applies before the new Codex session proceeds.
355
- - Uses a short timeout so Codex startup is not blocked for long when Machine1 is
355
+ - Uses a short timeout so Codex startup is not blocked for long when the master is
356
356
  offline.
357
357
  - Does not require a follower background service in v1.
358
358
 
@@ -405,7 +405,7 @@ Canonical names win by default. A same-name local MCP or skill fails unless
405
405
  12. Should master export rebuilds require a manual command or happen
406
406
  automatically? Decision: master watches selected paths and rebuilds
407
407
  automatically; manual rebuild exists only for repair/debugging.
408
- 13. Should Machine1 generate a permanent follower join link or require manual
408
+ 13. Should the master generate a permanent follower join link or require manual
409
409
  master address entry? Decision: generate a durable join link, with a
410
410
  copy-paste command fallback if custom URL handling is not implemented in the
411
411
  first version.
@@ -428,7 +428,7 @@ Canonical names win by default. A same-name local MCP or skill fails unless
428
428
 
429
429
  - macOS support.
430
430
  - GitHub-based plaintext secret sync.
431
- - Push-based orchestration from Machine1 to followers.
431
+ - Push-based orchestration from the master to followers.
432
432
  - Mid-session mutation of active Codex behavior.
433
433
  - Automatic promotion of follower-local changes.
434
434
  - Syncing Codex sessions, history, logs, or SQLite runtime state.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexport",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "sync a canonical Codex setup from one master machine to follower machines",
5
5
  "author": "Microck <contact@micr.dev>",
6
6
  "license": "MIT",