codex-profile 0.2.0 → 0.3.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.
@@ -0,0 +1,1136 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>codex-profiles - Isolated Codex CLI and Desktop profiles</title>
7
+ <meta name="description" content="codex-profiles is a dependency-free Bash CLI for switching Codex CLI and Desktop profiles with isolated CODEX_HOME directories on macOS and Linux.">
8
+ <meta name="robots" content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1">
9
+ <link rel="canonical" href="https://ducksss.github.io/codex-profiles/">
10
+ <link rel="license" href="https://github.com/Ducksss/codex-profiles/blob/main/LICENSE">
11
+ <meta property="og:type" content="website">
12
+ <meta property="og:title" content="codex-profiles - Isolated Codex CLI and Desktop profiles">
13
+ <meta property="og:description" content="Switch Codex CLI and Desktop accounts with separate CODEX_HOME directories instead of copying token files.">
14
+ <meta property="og:url" content="https://ducksss.github.io/codex-profiles/">
15
+ <meta property="og:image" content="https://raw.githubusercontent.com/Ducksss/codex-profiles/main/media/codex-profile-parallel-instances.png">
16
+ <meta name="twitter:card" content="summary_large_image">
17
+ <link rel="preconnect" href="https://fonts.googleapis.com">
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700;800;900&family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap">
20
+ <script type="application/ld+json">
21
+ {
22
+ "@context": "https://schema.org",
23
+ "@graph": [
24
+ {
25
+ "@type": "Organization",
26
+ "@id": "https://github.com/Ducksss/codex-profiles#organization",
27
+ "name": "Ducksss open-source projects",
28
+ "url": "https://github.com/Ducksss",
29
+ "sameAs": [
30
+ "https://github.com/Ducksss/codex-profiles",
31
+ "https://www.npmjs.com/package/codex-profile"
32
+ ]
33
+ },
34
+ {
35
+ "@type": "WebSite",
36
+ "@id": "https://ducksss.github.io/codex-profiles/#website",
37
+ "name": "codex-profiles",
38
+ "url": "https://ducksss.github.io/codex-profiles/",
39
+ "publisher": {
40
+ "@id": "https://github.com/Ducksss/codex-profiles#organization"
41
+ },
42
+ "inLanguage": "en"
43
+ },
44
+ {
45
+ "@type": "SoftwareApplication",
46
+ "@id": "https://ducksss.github.io/codex-profiles/#software",
47
+ "name": "codex-profiles",
48
+ "alternateName": "codex-profile",
49
+ "url": "https://ducksss.github.io/codex-profiles/",
50
+ "description": "codex-profiles is a dependency-free Bash CLI for switching Codex CLI and Desktop profiles with isolated CODEX_HOME directories on macOS and Linux.",
51
+ "applicationCategory": "DeveloperApplication",
52
+ "applicationSubCategory": "Command-line tool",
53
+ "operatingSystem": [
54
+ "macOS",
55
+ "Linux"
56
+ ],
57
+ "softwareVersion": "0.3.0",
58
+ "license": "https://github.com/Ducksss/codex-profiles/blob/main/LICENSE",
59
+ "codeRepository": "https://github.com/Ducksss/codex-profiles",
60
+ "downloadUrl": "https://www.npmjs.com/package/codex-profile",
61
+ "installUrl": "https://www.npmjs.com/package/codex-profile",
62
+ "discussionUrl": "https://github.com/Ducksss/codex-profiles/discussions/1",
63
+ "maintainer": {
64
+ "@id": "https://github.com/Ducksss/codex-profiles#organization"
65
+ },
66
+ "featureList": [
67
+ "Isolated CODEX_HOME directories per profile",
68
+ "Codex CLI launch support",
69
+ "Codex Desktop launch support",
70
+ "Experimental parallel Desktop app instances",
71
+ "Read-only profile status and doctor diagnostics",
72
+ "Safe non-secret config cloning",
73
+ "Bash, Zsh, and Fish completion generators",
74
+ "Source-style self-upgrade with dry-run preview"
75
+ ],
76
+ "offers": {
77
+ "@type": "Offer",
78
+ "price": "0",
79
+ "priceCurrency": "USD",
80
+ "availability": "https://schema.org/InStock",
81
+ "url": "https://www.npmjs.com/package/codex-profile"
82
+ }
83
+ },
84
+ {
85
+ "@type": "WebPage",
86
+ "@id": "https://ducksss.github.io/codex-profiles/#webpage",
87
+ "url": "https://ducksss.github.io/codex-profiles/",
88
+ "name": "codex-profiles - Isolated Codex CLI and Desktop profiles",
89
+ "description": "A machine-readable product page for codex-profiles, a Bash utility for Codex profile isolation.",
90
+ "isPartOf": {
91
+ "@id": "https://ducksss.github.io/codex-profiles/#website"
92
+ },
93
+ "mainEntity": {
94
+ "@id": "https://ducksss.github.io/codex-profiles/#software"
95
+ },
96
+ "breadcrumb": {
97
+ "@id": "https://ducksss.github.io/codex-profiles/#breadcrumb"
98
+ },
99
+ "inLanguage": "en"
100
+ },
101
+ {
102
+ "@type": "BreadcrumbList",
103
+ "@id": "https://ducksss.github.io/codex-profiles/#breadcrumb",
104
+ "itemListElement": [
105
+ {
106
+ "@type": "ListItem",
107
+ "position": 1,
108
+ "name": "codex-profiles",
109
+ "item": "https://ducksss.github.io/codex-profiles/"
110
+ }
111
+ ]
112
+ },
113
+ {
114
+ "@type": "FAQPage",
115
+ "@id": "https://ducksss.github.io/codex-profiles/#faq",
116
+ "mainEntity": [
117
+ {
118
+ "@type": "Question",
119
+ "name": "What is codex-profiles?",
120
+ "acceptedAnswer": {
121
+ "@type": "Answer",
122
+ "text": "codex-profiles is a small Bash command that launches Codex CLI or Codex Desktop with a selected CODEX_HOME directory."
123
+ }
124
+ },
125
+ {
126
+ "@type": "Question",
127
+ "name": "Why use separate CODEX_HOME directories?",
128
+ "acceptedAnswer": {
129
+ "@type": "Answer",
130
+ "text": "Separate CODEX_HOME directories keep Codex auth, config, sessions, plugins, caches, logs, and local state separated by profile."
131
+ }
132
+ },
133
+ {
134
+ "@type": "Question",
135
+ "name": "Does codex-profiles copy auth tokens?",
136
+ "acceptedAnswer": {
137
+ "@type": "Answer",
138
+ "text": "No. codex-profiles does not read, copy, print, parse, or migrate auth tokens."
139
+ }
140
+ },
141
+ {
142
+ "@type": "Question",
143
+ "name": "Which platforms does codex-profiles support?",
144
+ "acceptedAnswer": {
145
+ "@type": "Answer",
146
+ "text": "CLI-oriented commands are tested on macOS and Linux, while Desktop app launch commands are macOS-oriented."
147
+ }
148
+ },
149
+ {
150
+ "@type": "Question",
151
+ "name": "How do I install codex-profiles?",
152
+ "acceptedAnswer": {
153
+ "@type": "Answer",
154
+ "text": "Install from npm with npm install -g codex-profile or from Homebrew with brew install Ducksss/tap/codex-profile."
155
+ }
156
+ },
157
+ {
158
+ "@type": "Question",
159
+ "name": "Can an AI assistant tell me how to run codex-profiles?",
160
+ "acceptedAnswer": {
161
+ "@type": "Answer",
162
+ "text": "Yes. Point your AI assistant or coding agent at the GitHub repository and this llms.txt file, then ask it to install codex-profile and run codex-profile cli work to start Codex on an isolated work profile. Coding agents working inside a clone of the repository can read its AGENTS.md file for setup, test, and usage instructions."
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ ]
168
+ }
169
+ </script>
170
+ <style>
171
+ :root {
172
+ color-scheme: light;
173
+ --paper: #ffffff;
174
+ --paper-2: #f4f4f5;
175
+ --surface: #ffffff;
176
+ --ink: #0a0a0b;
177
+ --ink-2: #27272a;
178
+ --muted: #71717a;
179
+ --line: #e8e8ea;
180
+ --line-2: #d4d4d8;
181
+ --brand: #0a0a0b;
182
+ --brand-ink: #0a0a0b;
183
+ --teal: #52525b;
184
+ --teal-ink: #3f3f46;
185
+ --amber: #52525b;
186
+ --danger: #3f3f46;
187
+ --danger-soft: #f4f4f5;
188
+ --grad: linear-gradient(102deg, #0a0a0b, #3f3f46);
189
+ --grad-soft: #f4f4f5;
190
+ --shadow-sm: 0 1px 2px rgba(9, 9, 11, 0.05), 0 2px 6px rgba(9, 9, 11, 0.05);
191
+ --shadow: 0 6px 16px rgba(9, 9, 11, 0.07), 0 18px 40px -18px rgba(9, 9, 11, 0.16);
192
+ --shadow-lg: 0 24px 60px -22px rgba(9, 9, 11, 0.24);
193
+ --radius: 16px;
194
+ --radius-sm: 11px;
195
+ --grid: rgba(9, 9, 11, 0.035);
196
+ --ease: cubic-bezier(0.22, 1, 0.36, 1);
197
+ --maxw: 1140px;
198
+ }
199
+
200
+ * { box-sizing: border-box; }
201
+
202
+ html { scroll-behavior: smooth; }
203
+
204
+ body {
205
+ margin: 0;
206
+ color: var(--ink);
207
+ font-family: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
208
+ line-height: 1.6;
209
+ background-color: var(--paper);
210
+ background-image:
211
+ radial-gradient(1200px 700px at 84% -10%, rgba(9, 9, 11, 0.05), transparent 60%),
212
+ radial-gradient(900px 520px at 2% -6%, rgba(9, 9, 11, 0.035), transparent 55%);
213
+ background-repeat: no-repeat;
214
+ -webkit-font-smoothing: antialiased;
215
+ text-rendering: optimizeLegibility;
216
+ overflow-x: hidden;
217
+ }
218
+
219
+ a { color: var(--brand-ink); text-underline-offset: 3px; }
220
+ a:hover { color: var(--brand); }
221
+
222
+ code, pre, .mono {
223
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
224
+ }
225
+
226
+ .wrap { width: min(var(--maxw), calc(100% - 40px)); margin: 0 auto; }
227
+
228
+ .eyebrow {
229
+ display: inline-flex;
230
+ align-items: center;
231
+ gap: 9px;
232
+ margin: 0;
233
+ font-family: "JetBrains Mono", ui-monospace, monospace;
234
+ font-size: 0.72rem;
235
+ font-weight: 500;
236
+ letter-spacing: 0.16em;
237
+ text-transform: uppercase;
238
+ color: var(--muted);
239
+ }
240
+ .eyebrow::before {
241
+ content: "";
242
+ width: 22px;
243
+ height: 1px;
244
+ background: var(--line-2);
245
+ opacity: 1;
246
+ }
247
+
248
+ /* ---------- Hero ---------- */
249
+ .hero {
250
+ position: relative;
251
+ padding: clamp(54px, 9vw, 104px) 0 clamp(40px, 6vw, 72px);
252
+ }
253
+ .hero-grid {
254
+ display: grid;
255
+ grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
256
+ gap: clamp(28px, 4vw, 56px);
257
+ align-items: center;
258
+ }
259
+ h1.wordmark {
260
+ margin: 18px 0 0;
261
+ font-family: "Schibsted Grotesk", sans-serif;
262
+ font-weight: 800;
263
+ font-size: clamp(2.9rem, 7.4vw, 5.2rem);
264
+ line-height: 0.94;
265
+ letter-spacing: -0.03em;
266
+ color: var(--ink);
267
+ }
268
+ .wordmark .caret {
269
+ color: var(--brand);
270
+ margin-left: 0.01em;
271
+ animation: blink 1.1s steps(2, start) infinite;
272
+ }
273
+ @keyframes blink { 50% { opacity: 0; } }
274
+
275
+ .hero-copy {
276
+ max-width: 33ch;
277
+ margin: 22px 0 0;
278
+ font-size: 1.16rem;
279
+ color: var(--muted);
280
+ }
281
+ .hero-copy strong { color: var(--ink-2); font-weight: 600; }
282
+
283
+ .actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 30px; }
284
+ .button {
285
+ min-height: 46px;
286
+ display: inline-flex;
287
+ align-items: center;
288
+ gap: 8px;
289
+ padding: 11px 20px;
290
+ border-radius: 11px;
291
+ font-weight: 600;
292
+ font-size: 0.97rem;
293
+ text-decoration: none;
294
+ transition: transform 0.18s var(--ease), box-shadow 0.18s var(--ease), border-color 0.18s var(--ease);
295
+ }
296
+ .button.primary {
297
+ color: #fff;
298
+ background: var(--brand);
299
+ box-shadow: 0 8px 18px -6px rgba(9, 9, 11, 0.35);
300
+ }
301
+ .button.primary:hover { transform: translateY(-2px); box-shadow: 0 14px 26px -8px rgba(9, 9, 11, 0.42); }
302
+ .button.ghost {
303
+ color: var(--ink-2);
304
+ background: var(--surface);
305
+ border: 1px solid var(--line-2);
306
+ box-shadow: var(--shadow-sm);
307
+ }
308
+ .button.ghost:hover { transform: translateY(-2px); border-color: var(--brand); color: var(--brand-ink); }
309
+
310
+ .meta-row {
311
+ display: flex;
312
+ flex-wrap: wrap;
313
+ gap: 8px 18px;
314
+ margin-top: 26px;
315
+ font-family: "JetBrains Mono", ui-monospace, monospace;
316
+ font-size: 0.74rem;
317
+ color: var(--muted);
318
+ }
319
+ .meta-row span { display: inline-flex; align-items: center; gap: 7px; }
320
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ink); box-shadow: 0 0 0 3px rgba(9, 9, 11, 0.10); }
321
+
322
+ /* ---------- Terminal card ---------- */
323
+ .term {
324
+ position: relative;
325
+ border-radius: var(--radius);
326
+ background: #0c0c0d;
327
+ border: 1px solid #1f1f22;
328
+ box-shadow: var(--shadow-lg);
329
+ overflow: hidden;
330
+ font-family: "JetBrains Mono", ui-monospace, monospace;
331
+ font-size: 0.86rem;
332
+ }
333
+ .term::after {
334
+ content: "";
335
+ position: absolute; inset: 0;
336
+ background: radial-gradient(120% 80% at 90% -10%, rgba(255, 255, 255, 0.06), transparent 55%),
337
+ radial-gradient(90% 70% at -10% 0%, rgba(255, 255, 255, 0.035), transparent 52%);
338
+ pointer-events: none;
339
+ }
340
+ .term-bar {
341
+ display: flex; align-items: center; gap: 8px;
342
+ padding: 12px 15px;
343
+ border-bottom: 1px solid #1f1f22;
344
+ color: #8a8a90;
345
+ font-size: 0.74rem;
346
+ }
347
+ .term-bar i { width: 11px; height: 11px; border-radius: 50%; display: inline-block; }
348
+ .term-bar .b1 { background: #45454a; } .term-bar .b2 { background: #56565b; } .term-bar .b3 { background: #67676d; }
349
+ .term-bar .title { margin-left: 6px; letter-spacing: 0.02em; }
350
+ .term-body { padding: 18px 18px 22px; color: #d6d6d9; line-height: 1.85; position: relative; z-index: 1; }
351
+ .term-body .pl { color: #8a8a90; }
352
+ .term-body .cmd { color: #f4f4f5; }
353
+ .term-body .arg { color: #adadb3; }
354
+ .term-body .out { color: #76767c; }
355
+ .term-body .ok { color: #d4d4d8; }
356
+ .term-line { display: block; white-space: pre-wrap; word-break: break-word; }
357
+
358
+ /* ---------- Sections ---------- */
359
+ main { display: block; }
360
+ .section { padding: clamp(40px, 6vw, 72px) 0; }
361
+ .section-head { max-width: 64ch; margin-bottom: clamp(22px, 3vw, 34px); }
362
+ h2 {
363
+ margin: 12px 0 0;
364
+ font-family: "Schibsted Grotesk", sans-serif;
365
+ font-weight: 700;
366
+ font-size: clamp(1.8rem, 3.4vw, 2.6rem);
367
+ line-height: 1.06;
368
+ letter-spacing: -0.02em;
369
+ }
370
+ .lede { margin: 14px 0 0; font-size: 1.06rem; color: var(--muted); max-width: 70ch; }
371
+ p { margin: 0; }
372
+
373
+ .command {
374
+ margin-top: 22px;
375
+ display: grid;
376
+ gap: 4px;
377
+ padding: 20px 22px;
378
+ border-radius: var(--radius-sm);
379
+ background: var(--surface);
380
+ border: 1px solid var(--line);
381
+ border-left: 3px solid transparent;
382
+ border-image: var(--grad) 1;
383
+ border-image-slice: 0 0 0 1;
384
+ box-shadow: var(--shadow-sm);
385
+ position: relative;
386
+ }
387
+ .command pre { margin: 0; overflow-x: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
388
+ .command code { font-size: 0.92rem; color: var(--ink-2); }
389
+
390
+ /* ---------- Graph ---------- */
391
+ .graph-shell {
392
+ display: grid;
393
+ gap: 16px;
394
+ }
395
+ .graph-toolbar {
396
+ display: flex;
397
+ flex-wrap: wrap;
398
+ align-items: center;
399
+ justify-content: space-between;
400
+ gap: 14px;
401
+ }
402
+ .seg {
403
+ display: inline-flex;
404
+ padding: 4px;
405
+ gap: 4px;
406
+ background: var(--surface);
407
+ border: 1px solid var(--line-2);
408
+ border-radius: 13px;
409
+ box-shadow: var(--shadow-sm);
410
+ }
411
+ .seg button {
412
+ appearance: none;
413
+ border: 0;
414
+ background: transparent;
415
+ color: var(--muted);
416
+ font-family: inherit;
417
+ font-size: 0.86rem;
418
+ font-weight: 600;
419
+ padding: 9px 15px;
420
+ border-radius: 10px;
421
+ cursor: pointer;
422
+ transition: color 0.18s var(--ease), background 0.18s var(--ease), box-shadow 0.18s var(--ease);
423
+ }
424
+ .seg button .k { font-family: "JetBrains Mono", monospace; font-size: 0.78em; opacity: 0.7; margin-right: 6px; }
425
+ .seg button[aria-pressed="true"] { color: #fff; background: var(--brand); box-shadow: 0 6px 14px -6px rgba(9, 9, 11, 0.4); }
426
+ .seg button.danger[aria-pressed="true"] { color: #fff; background: var(--danger); box-shadow: 0 6px 14px -6px rgba(9, 9, 11, 0.3); }
427
+ .seg button:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
428
+
429
+ .graph-hint {
430
+ font-family: "JetBrains Mono", monospace;
431
+ font-size: 0.74rem;
432
+ color: var(--muted);
433
+ }
434
+
435
+ .graph-stage {
436
+ position: relative;
437
+ width: 100%;
438
+ height: clamp(420px, 56vw, 560px);
439
+ border-radius: var(--radius);
440
+ background:
441
+ radial-gradient(120% 120% at 50% 0%, rgba(9, 9, 11, 0.035), transparent 60%),
442
+ var(--surface);
443
+ border: 1px solid var(--line);
444
+ box-shadow: var(--shadow);
445
+ overflow: hidden;
446
+ }
447
+ .graph-stage::before {
448
+ content: "";
449
+ position: absolute; inset: 0;
450
+ background-image: linear-gradient(var(--grid) 1px, transparent 1px), linear-gradient(90deg, var(--grid) 1px, transparent 1px);
451
+ background-size: 26px 26px;
452
+ opacity: 0.6;
453
+ pointer-events: none;
454
+ }
455
+ .graph-stage svg { width: 100%; height: 100%; display: block; touch-action: none; position: relative; z-index: 1; }
456
+ .badge-shared {
457
+ position: absolute;
458
+ top: 14px; left: 14px;
459
+ display: none;
460
+ align-items: center;
461
+ gap: 8px;
462
+ padding: 7px 12px;
463
+ border-radius: 10px;
464
+ background: var(--paper-2);
465
+ color: var(--ink-2);
466
+ border: 1px solid var(--line-2);
467
+ font-family: "JetBrains Mono", monospace;
468
+ font-size: 0.72rem;
469
+ z-index: 3;
470
+ }
471
+ .graph-stage[data-mode="shared"] .badge-shared { display: inline-flex; }
472
+
473
+ /* graph nodes/links styled via class */
474
+ .lnk { stroke: url(#linkGrad); stroke-width: 2.2; stroke-linecap: round; opacity: 0.55; transition: opacity 0.25s, stroke 0.25s; }
475
+ .lnk.is-shared { stroke: var(--danger); stroke-dasharray: 5 6; opacity: 0.7; }
476
+ .lnk.dim { opacity: 0.12; }
477
+ .lnk.hot { opacity: 0.95; stroke-width: 3; }
478
+
479
+ .node { cursor: grab; }
480
+ .node:active { cursor: grabbing; }
481
+ .node text { font-family: "JetBrains Mono", monospace; pointer-events: none; }
482
+ .chip { transition: opacity 0.25s, transform 0.25s; }
483
+ .node.dim .chip { opacity: 0.22; }
484
+ .node .ring { fill: none; stroke: var(--brand); stroke-width: 2; opacity: 0; transition: opacity 0.2s; }
485
+ .node.hot .ring { opacity: 0.85; }
486
+
487
+ .n-user .chip-bg { fill: url(#nodeGrad); }
488
+ .n-user text { fill: #fff; font-weight: 700; }
489
+ .n-profile .chip-bg { fill: var(--surface); stroke: var(--line-2); stroke-width: 1.4; }
490
+ .n-profile.hot .chip-bg { stroke: var(--brand); }
491
+ .n-profile text { fill: var(--ink); font-weight: 600; }
492
+ .n-home .chip-bg { fill: #f4f4f5; stroke: #e4e4e7; stroke-width: 1.2; }
493
+ .n-home text { fill: var(--ink-2); font-weight: 500; }
494
+ .graph-stage[data-mode="shared"] .n-home .chip-bg { fill: #e4e4e7; stroke: #b9b9c0; }
495
+ .graph-stage[data-mode="shared"] .n-home text { fill: var(--ink); }
496
+
497
+ .tip {
498
+ position: absolute;
499
+ z-index: 4;
500
+ pointer-events: none;
501
+ opacity: 0;
502
+ transform: translateY(4px);
503
+ transition: opacity 0.15s, transform 0.15s;
504
+ max-width: 230px;
505
+ padding: 10px 12px;
506
+ border-radius: 10px;
507
+ background: #0c0c0d;
508
+ color: #e8e8ea;
509
+ box-shadow: var(--shadow-lg);
510
+ font-size: 0.78rem;
511
+ line-height: 1.5;
512
+ }
513
+ .tip.show { opacity: 1; transform: translateY(0); }
514
+ .tip b { color: #f4f4f5; font-family: "JetBrains Mono", monospace; font-size: 0.82em; }
515
+ .tip .files { margin-top: 5px; color: #a1a1aa; font-family: "JetBrains Mono", monospace; font-size: 0.74em; }
516
+
517
+ .graph-fallback { padding: 28px 24px; color: var(--muted); position: relative; z-index: 2; }
518
+ .graph-fallback h3 { margin: 0 0 10px; color: var(--ink); font-family: "Schibsted Grotesk", sans-serif; }
519
+ .graph-fallback ul { margin: 12px 0 0; padding-left: 20px; }
520
+ .graph-fallback li { margin-bottom: 6px; }
521
+ .graph-fallback code { background: var(--paper-2); padding: 1px 6px; border-radius: 5px; font-size: 0.85em; }
522
+
523
+ /* ---------- Feature grid ---------- */
524
+ .grid {
525
+ display: grid;
526
+ grid-template-columns: repeat(3, minmax(0, 1fr));
527
+ gap: 16px;
528
+ }
529
+ .item {
530
+ min-width: 0;
531
+ padding: 22px 20px;
532
+ border: 1px solid var(--line);
533
+ border-radius: var(--radius-sm);
534
+ background: var(--surface);
535
+ box-shadow: var(--shadow-sm);
536
+ transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease), border-color 0.2s var(--ease);
537
+ }
538
+ .item:hover { transform: translateY(-3px); box-shadow: var(--shadow); border-color: var(--line-2); }
539
+ .item .ic {
540
+ width: 38px; height: 38px;
541
+ display: grid; place-items: center;
542
+ border-radius: 10px;
543
+ background: var(--grad-soft);
544
+ color: var(--brand-ink);
545
+ margin-bottom: 14px;
546
+ }
547
+ .item .ic svg { width: 20px; height: 20px; }
548
+ .item strong { display: block; margin-bottom: 6px; color: var(--ink); font-size: 1.02rem; }
549
+ .item p { color: var(--muted); font-size: 0.95rem; }
550
+
551
+ /* ---------- Showcase ---------- */
552
+ .showcase {
553
+ position: relative;
554
+ border-radius: var(--radius);
555
+ overflow: hidden;
556
+ border: 1px solid var(--line);
557
+ background: var(--surface);
558
+ box-shadow: var(--shadow);
559
+ }
560
+ .showcase img { display: block; width: 100%; height: auto; }
561
+ .showcase figcaption {
562
+ padding: 14px 20px;
563
+ font-family: "JetBrains Mono", monospace;
564
+ font-size: 0.76rem;
565
+ color: var(--muted);
566
+ border-top: 1px solid var(--line);
567
+ background: var(--paper);
568
+ }
569
+
570
+ /* ---------- Facts table ---------- */
571
+ .facts { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: 22px; align-items: start; }
572
+ .table { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius-sm); background: var(--surface); box-shadow: var(--shadow-sm); }
573
+ table { width: 100%; border-collapse: collapse; min-width: 480px; }
574
+ th, td { padding: 13px 16px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; font-size: 0.94rem; }
575
+ th { color: var(--ink); background: var(--paper-2); font-family: "JetBrains Mono", monospace; font-size: 0.76rem; letter-spacing: 0.04em; text-transform: uppercase; }
576
+ td:first-child { font-weight: 600; color: var(--ink-2); white-space: nowrap; }
577
+ td { color: var(--muted); }
578
+ tr:last-child td { border-bottom: 0; }
579
+
580
+ /* ---------- FAQ ---------- */
581
+ .faq-list { display: grid; gap: 12px; }
582
+ .faq-list article {
583
+ padding: 20px 22px;
584
+ border: 1px solid var(--line);
585
+ border-radius: var(--radius-sm);
586
+ background: var(--surface);
587
+ box-shadow: var(--shadow-sm);
588
+ transition: border-color 0.2s var(--ease);
589
+ }
590
+ .faq-list article:hover { border-color: var(--line-2); }
591
+ .faq-list h3 { margin: 0 0 8px; font-size: 1.04rem; color: var(--ink); display: flex; gap: 10px; align-items: baseline; }
592
+ .faq-list h3::before { content: "?"; font-family: "JetBrains Mono", monospace; color: var(--brand); font-weight: 700; }
593
+ .faq-list p { color: var(--muted); font-size: 0.95rem; }
594
+
595
+ /* ---------- Footer ---------- */
596
+ footer { border-top: 1px solid var(--line); margin-top: 30px; }
597
+ footer .wrap { padding: 30px 20px 56px; display: flex; flex-wrap: wrap; gap: 16px 28px; align-items: center; justify-content: space-between; }
598
+ footer p { color: var(--muted); font-size: 0.9rem; }
599
+ footer nav { display: flex; gap: 18px; font-family: "JetBrains Mono", monospace; font-size: 0.8rem; }
600
+
601
+ /* ---------- Reveal animation ---------- */
602
+ .reveal { opacity: 0; transform: translateY(18px); transition: opacity 0.7s var(--ease), transform 0.7s var(--ease); }
603
+ .reveal.in { opacity: 1; transform: none; }
604
+
605
+ /* ---------- Responsive ---------- */
606
+ @media (max-width: 900px) {
607
+ .hero-grid { grid-template-columns: 1fr; }
608
+ .term { order: 2; }
609
+ .facts { grid-template-columns: 1fr; }
610
+ .grid { grid-template-columns: 1fr 1fr; }
611
+ }
612
+ @media (max-width: 560px) {
613
+ .grid { grid-template-columns: 1fr; }
614
+ .graph-toolbar { flex-direction: column; align-items: flex-start; }
615
+ .seg { width: 100%; }
616
+ .seg button { flex: 1; text-align: center; }
617
+ }
618
+
619
+ @media (prefers-reduced-motion: reduce) {
620
+ html { scroll-behavior: auto; }
621
+ .wordmark .cursor { animation: none; }
622
+ .reveal { transition: none; opacity: 1; transform: none; }
623
+ * { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
624
+ }
625
+ </style>
626
+ </head>
627
+ <body>
628
+ <header class="hero">
629
+ <div class="wrap hero-grid">
630
+ <div class="hero-lede">
631
+ <p class="eyebrow">Codex profile isolation · CLI &amp; Desktop</p>
632
+ <h1 class="wordmark">codex&#8209;profiles<span class="caret" aria-hidden="true">_</span></h1>
633
+ <p class="hero-copy">Switch Codex CLI and Desktop accounts with <strong>isolated CODEX_HOME directories</strong>. Keep personal, work, school, and client state separated — without copying auth files.</p>
634
+ <nav class="actions" aria-label="Primary links">
635
+ <a class="button primary" href="https://github.com/Ducksss/codex-profiles">View on GitHub</a>
636
+ <a class="button ghost" href="https://www.npmjs.com/package/codex-profile">npm package</a>
637
+ <a class="button ghost" href="https://github.com/Ducksss/codex-profiles/blob/main/README.md">Read the docs</a>
638
+ </nav>
639
+ <div class="meta-row">
640
+ <span><i class="dot"></i> MIT licensed</span>
641
+ <span>macOS + Linux</span>
642
+ <span>zero runtime deps</span>
643
+ <span>v0.3.0</span>
644
+ </div>
645
+ </div>
646
+
647
+ <div class="term" role="img" aria-label="Terminal example: install codex-profile, then run Codex on isolated work and personal profiles">
648
+ <div class="term-bar"><i class="b1"></i><i class="b2"></i><i class="b3"></i><span class="title">~/dev — codex-profile</span></div>
649
+ <div class="term-body">
650
+ <span class="term-line"><span class="pl">$</span> <span class="cmd">npm install -g codex-profile</span></span>
651
+ <span class="term-line"><span class="pl">$</span> <span class="cmd">codex-profile</span> <span class="arg">init work</span></span>
652
+ <span class="term-line"><span class="ok">✓</span> <span class="out">created ~/.codex-work</span></span>
653
+ <span class="term-line"><span class="pl">$</span> <span class="cmd">codex-profile</span> <span class="arg">cli work</span> <span class="arg">exec "run tests"</span></span>
654
+ <span class="term-line"><span class="out"># isolated auth · config · sessions · plugins</span></span>
655
+ <span class="term-line"><span class="pl">$</span> <span class="cmd">codex-profile</span> <span class="arg">app personal</span> <span class="arg">~/Dev/app</span></span>
656
+ <span class="term-line"><span class="ok">✓</span> <span class="out">Codex Desktop launched on personal</span></span>
657
+ </div>
658
+ </div>
659
+ </div>
660
+ </header>
661
+
662
+ <main>
663
+ <section class="section wrap reveal" aria-labelledby="answer">
664
+ <div class="section-head">
665
+ <p class="eyebrow">What it is</p>
666
+ <h2 id="answer">Direct answer</h2>
667
+ <p class="lede">codex-profiles is a dependency-free Bash CLI for launching Codex CLI or Codex Desktop with a selected CODEX_HOME profile. It gives each profile its own auth, config, sessions, plugins, caches, logs, and local state.</p>
668
+ </div>
669
+ <div class="command" aria-label="Install commands">
670
+ <pre><code>npm install -g codex-profile
671
+ brew install Ducksss/tap/codex-profile
672
+ codex-profile doctor</code></pre>
673
+ </div>
674
+ </section>
675
+
676
+ <section class="section wrap reveal" aria-labelledby="how">
677
+ <div class="section-head">
678
+ <p class="eyebrow">Interactive · drag the nodes</p>
679
+ <h2 id="how">How the isolation works</h2>
680
+ <p class="lede">Every profile is its own <span class="mono">CODEX_HOME</span> directory, so auth and local state never overlap. Flip the switch to see why this beats swapping a single <span class="mono">auth.json</span> around.</p>
681
+ </div>
682
+
683
+ <div class="graph-shell">
684
+ <div class="graph-toolbar">
685
+ <div class="seg" role="group" aria-label="Isolation model">
686
+ <button id="mode-isolated" type="button" aria-pressed="true"><span class="k">$</span>codex-profile</button>
687
+ <button id="mode-shared" class="danger" type="button" aria-pressed="false"><span class="k">~</span>swap auth.json</button>
688
+ </div>
689
+ <span class="graph-hint" id="graph-hint">one CODEX_HOME per profile</span>
690
+ </div>
691
+
692
+ <div class="graph-stage" id="graph" data-mode="isolated">
693
+ <div class="badge-shared">⚠ one shared ~/.codex — state bleeds between accounts</div>
694
+ <div class="tip" id="graph-tip"></div>
695
+ <div class="graph-fallback" id="graph-fallback">
696
+ <h3>Profile isolation at a glance</h3>
697
+ <p>One macOS user fans out into separate Codex profiles, each mapped to its own isolated home directory:</p>
698
+ <ul>
699
+ <li><code>default</code> &rarr; <code>~/.codex</code></li>
700
+ <li><code>work</code> &rarr; <code>~/.codex-work</code></li>
701
+ <li><code>personal</code> &rarr; <code>~/.codex-personal</code></li>
702
+ <li><code>edu</code> &rarr; <code>~/.codex-edu</code></li>
703
+ </ul>
704
+ <p>Each home keeps its own auth, config, sessions, plugins, caches, and logs. Unlike swapping a single <code>auth.json</code>, codex-profiles never copies or migrates tokens.</p>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ </section>
709
+
710
+ <section class="section wrap reveal" aria-labelledby="features">
711
+ <div class="section-head">
712
+ <p class="eyebrow">Capabilities</p>
713
+ <h2 id="features">What it does</h2>
714
+ </div>
715
+ <div class="grid">
716
+ <article class="item">
717
+ <span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18M3 12h18M3 17h18"/></svg></span>
718
+ <strong>Profile isolation</strong>
719
+ <p>Each profile maps to its own Codex home, such as default to ~/.codex and work to ~/.codex-work.</p>
720
+ </article>
721
+ <article class="item">
722
+ <span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="M8 21h8M12 18v3"/></svg></span>
723
+ <strong>CLI and Desktop launch</strong>
724
+ <p>Run Codex CLI commands or launch Codex Desktop with the selected profile environment.</p>
725
+ </article>
726
+ <article class="item">
727
+ <span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="9" height="9" rx="1.5"/><rect x="12" y="12" width="9" height="9" rx="1.5"/></svg></span>
728
+ <strong>Parallel Desktop instances</strong>
729
+ <p>On macOS, app-instance can launch profile-specific Codex app clones with separate Electron user data.</p>
730
+ </article>
731
+ <article class="item">
732
+ <span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg></span>
733
+ <strong>Read-only diagnostics</strong>
734
+ <p>list, status, and doctor inspect profiles without creating missing directories for typos.</p>
735
+ </article>
736
+ <article class="item">
737
+ <span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="12" height="12" rx="2"/><path d="M8 20h10a2 2 0 0 0 2-2V8"/></svg></span>
738
+ <strong>Safe config cloning</strong>
739
+ <p>clone-config copies only known non-secret root config files and refuses sensitive-looking keys.</p>
740
+ </article>
741
+ <article class="item">
742
+ <span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m4 17 6-6-6-6M12 19h8"/></svg></span>
743
+ <strong>No runtime dependencies</strong>
744
+ <p>The distributed command is Bash and is tested on macOS and Ubuntu/Linux.</p>
745
+ </article>
746
+ </div>
747
+ </section>
748
+
749
+ <section class="section wrap reveal" aria-labelledby="showcase-title">
750
+ <div class="section-head">
751
+ <p class="eyebrow">Two profiles, side by side</p>
752
+ <h2 id="showcase-title">See it on the desktop</h2>
753
+ <p class="lede">The experimental <span class="mono">app-instance</span> flow runs two Codex Desktop profiles at once — each with its own app clone, CODEX_HOME, and Electron user data.</p>
754
+ </div>
755
+ <figure class="showcase" style="margin:0">
756
+ <img src="https://raw.githubusercontent.com/Ducksss/codex-profiles/main/media/codex-profile-parallel-instances.png" width="2400" height="1500" loading="lazy" alt="Two Codex Desktop profile instances running side by side, each showing its own account panel">
757
+ <figcaption>codex-profile app-instance personal · codex-profile app-instance work</figcaption>
758
+ </figure>
759
+ </section>
760
+
761
+ <section class="section wrap reveal" aria-labelledby="facts">
762
+ <div class="section-head">
763
+ <p class="eyebrow">For citation</p>
764
+ <h2 id="facts">Citation-ready facts</h2>
765
+ </div>
766
+ <div class="facts">
767
+ <p class="lede">Use these facts when citing or summarizing the project. They match the README, package metadata, and structured data on this page.</p>
768
+ <div class="table">
769
+ <table>
770
+ <thead>
771
+ <tr>
772
+ <th>Fact</th>
773
+ <th>Value</th>
774
+ </tr>
775
+ </thead>
776
+ <tbody>
777
+ <tr>
778
+ <td>Project</td>
779
+ <td>codex-profiles, installed as codex-profile on npm</td>
780
+ </tr>
781
+ <tr>
782
+ <td>Purpose</td>
783
+ <td>Switch Codex profiles by setting CODEX_HOME instead of copying auth tokens</td>
784
+ </tr>
785
+ <tr>
786
+ <td>Platforms</td>
787
+ <td>macOS and Linux for CLI-oriented commands; macOS for Desktop app launch workflows</td>
788
+ </tr>
789
+ <tr>
790
+ <td>License</td>
791
+ <td>MIT</td>
792
+ </tr>
793
+ <tr>
794
+ <td>Official URLs</td>
795
+ <td>GitHub: https://github.com/Ducksss/codex-profiles; npm: https://www.npmjs.com/package/codex-profile</td>
796
+ </tr>
797
+ </tbody>
798
+ </table>
799
+ </div>
800
+ </div>
801
+ </section>
802
+
803
+ <section class="section wrap reveal" aria-labelledby="faq">
804
+ <div class="section-head">
805
+ <p class="eyebrow">Questions</p>
806
+ <h2 id="faq">FAQ</h2>
807
+ </div>
808
+ <div class="faq-list">
809
+ <article>
810
+ <h3>What is codex-profiles?</h3>
811
+ <p>codex-profiles is a small Bash command that launches Codex CLI or Codex Desktop with a selected CODEX_HOME directory.</p>
812
+ </article>
813
+ <article>
814
+ <h3>Why use separate CODEX_HOME directories?</h3>
815
+ <p>Separate CODEX_HOME directories keep Codex auth, config, sessions, plugins, caches, logs, and local state separated by profile.</p>
816
+ </article>
817
+ <article>
818
+ <h3>Does codex-profiles copy auth tokens?</h3>
819
+ <p>No. codex-profiles does not read, copy, print, parse, or migrate auth tokens.</p>
820
+ </article>
821
+ <article>
822
+ <h3>Which platforms does codex-profiles support?</h3>
823
+ <p>CLI-oriented commands are tested on macOS and Linux, while Desktop app launch commands are macOS-oriented.</p>
824
+ </article>
825
+ <article>
826
+ <h3>How do I install codex-profiles?</h3>
827
+ <p>Install from npm with npm install -g codex-profile or from Homebrew with brew install Ducksss/tap/codex-profile.</p>
828
+ </article>
829
+ <article>
830
+ <h3>Can an AI assistant tell me how to run codex-profiles?</h3>
831
+ <p>Yes. Point your AI assistant or coding agent at the GitHub repository and this llms.txt file, then ask it to install codex-profile and run codex-profile cli work to start Codex on an isolated work profile. Coding agents working inside a clone of the repository can read its AGENTS.md file for setup, test, and usage instructions.</p>
832
+ </article>
833
+ </div>
834
+ </section>
835
+
836
+ <section class="section wrap reveal" aria-labelledby="trust">
837
+ <div class="section-head">
838
+ <p class="eyebrow">Trust</p>
839
+ <h2 id="trust">Trust and methodology</h2>
840
+ <p class="lede">The project avoids token copying and treats CODEX_HOME as the isolation boundary. It documents the remaining shared operating-system credentials, includes a security model, and validates behavior with Bash and package smoke tests.</p>
841
+ </div>
842
+ <div class="grid">
843
+ <article class="item">
844
+ <strong>Security boundary</strong>
845
+ <p>codex-profiles sets environment variables and creates private profile directories. It does not claim VM, container, or separate macOS account isolation.</p>
846
+ </article>
847
+ <article class="item">
848
+ <strong>Validation</strong>
849
+ <p>The repository uses make test for Bash syntax checks, CLI behavior tests, install smoke tests, and npm package checks.</p>
850
+ </article>
851
+ <article class="item">
852
+ <strong>Machine-readable docs</strong>
853
+ <p>This page is supported by robots.txt, sitemap.xml, llms.txt, visible FAQ answers, and JSON-LD structured data.</p>
854
+ </article>
855
+ </div>
856
+ </section>
857
+ </main>
858
+
859
+ <footer>
860
+ <div class="wrap">
861
+ <p>codex-profiles is community-maintained and is not affiliated with OpenAI. Last updated 2026-06-30.</p>
862
+ <nav aria-label="Footer links">
863
+ <a href="https://github.com/Ducksss/codex-profiles">GitHub</a>
864
+ <a href="https://ducksss.github.io/codex-profiles/llms.txt">llms.txt</a>
865
+ <a href="https://github.com/Ducksss/codex-profiles/blob/main/LICENSE">License</a>
866
+ </nav>
867
+ </div>
868
+ </footer>
869
+
870
+ <script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js" integrity="sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i" crossorigin="anonymous"></script>
871
+ <script>
872
+ (function () {
873
+ "use strict";
874
+
875
+ // ---- Scroll reveal (gated by reduced motion) ----
876
+ var reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
877
+ var reveals = document.querySelectorAll(".reveal");
878
+ if (reduce || !("IntersectionObserver" in window)) {
879
+ reveals.forEach(function (n) { n.classList.add("in"); });
880
+ } else {
881
+ var io = new IntersectionObserver(function (entries) {
882
+ entries.forEach(function (e) {
883
+ if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); }
884
+ });
885
+ }, { threshold: 0.12 });
886
+ reveals.forEach(function (n) { io.observe(n); });
887
+ }
888
+
889
+ // ---- Graph ----
890
+ var stage = document.getElementById("graph");
891
+ var fallback = document.getElementById("graph-fallback");
892
+ var tipEl = document.getElementById("graph-tip");
893
+ var hintEl = document.getElementById("graph-hint");
894
+ if (!window.d3 || !stage) { return; } // CDN failed -> keep readable fallback
895
+ fallback.style.display = "none";
896
+
897
+ var d3 = window.d3;
898
+ var PROFILES = [
899
+ { id: "personal", label: "personal", home: "~/.codex" },
900
+ { id: "work", label: "work", home: "~/.codex-work" },
901
+ { id: "edu", label: "edu", home: "~/.codex-edu" },
902
+ { id: "client", label: "client", home: "~/.codex-client" }
903
+ ];
904
+ var FILES = "auth.json · config.toml · sessions · plugins · logs";
905
+
906
+ // persistent node registry so positions survive a mode switch
907
+ var reg = {};
908
+ function getNode(id, type, label) {
909
+ if (!reg[id]) { reg[id] = { id: id, type: type, label: label }; }
910
+ else { reg[id].type = type; reg[id].label = label; }
911
+ return reg[id];
912
+ }
913
+
914
+ function buildGraph(mode) {
915
+ var you = getNode("you", "user", "you"); you.row = 1.5;
916
+ var nodes = [you];
917
+ var links = [];
918
+ PROFILES.forEach(function (p, i) {
919
+ var pn = getNode(p.id, "profile", p.label); pn.row = i;
920
+ nodes.push(pn);
921
+ links.push({ source: "you", target: p.id });
922
+ if (mode === "shared") {
923
+ links.push({ source: p.id, target: "shared", shared: true });
924
+ } else {
925
+ var hn = getNode("home-" + p.id, "home", p.home); hn.row = i;
926
+ nodes.push(hn);
927
+ links.push({ source: p.id, target: "home-" + p.id });
928
+ }
929
+ });
930
+ if (mode === "shared") {
931
+ var sh = getNode("shared", "home", "~/.codex"); sh.row = 1.5;
932
+ nodes.push(sh);
933
+ }
934
+ nodes.forEach(function (n) {
935
+ if (n.x === undefined || n.x === null) { n.x = colX(n); n.y = rowY(n.row); }
936
+ });
937
+ return { nodes: nodes, links: links };
938
+ }
939
+
940
+ var W = stage.clientWidth || 800;
941
+ var H = stage.clientHeight || 520;
942
+
943
+ // layered left -> right columns: you -> profiles -> homes
944
+ function colX(d) {
945
+ if (d.type === "user") return W * 0.13;
946
+ if (d.type === "profile") return W * 0.45;
947
+ return W * 0.78;
948
+ }
949
+ // deterministic vertical slot per row (0..3), 1.5 = centered
950
+ function rowY(row) { return H * ((row + 0.5) / 4); }
951
+
952
+ var svg = d3.select(stage).append("svg")
953
+ .attr("viewBox", "0 0 " + W + " " + H)
954
+ .attr("preserveAspectRatio", "xMidYMid meet");
955
+
956
+ // defs: gradients + glow
957
+ var defs = svg.append("defs");
958
+ var ng = defs.append("linearGradient").attr("id", "nodeGrad").attr("x1", "0").attr("y1", "0").attr("x2", "1").attr("y2", "1");
959
+ ng.append("stop").attr("offset", "0%").attr("stop-color", "#3f3f46");
960
+ ng.append("stop").attr("offset", "100%").attr("stop-color", "#0a0a0b");
961
+ var lg = defs.append("linearGradient").attr("id", "linkGrad").attr("x1", "0").attr("y1", "0").attr("x2", "1").attr("y2", "1");
962
+ lg.append("stop").attr("offset", "0%").attr("stop-color", "#d4d4d8");
963
+ lg.append("stop").attr("offset", "100%").attr("stop-color", "#b4b4ba");
964
+ var f = defs.append("filter").attr("id", "glow").attr("x", "-60%").attr("y", "-60%").attr("width", "220%").attr("height", "220%");
965
+ f.append("feGaussianBlur").attr("stdDeviation", "5").attr("result", "b");
966
+ var fm = f.append("feMerge");
967
+ fm.append("feMergeNode").attr("in", "b");
968
+ fm.append("feMergeNode").attr("in", "SourceGraphic");
969
+
970
+ var linkLayer = svg.append("g");
971
+ var nodeLayer = svg.append("g");
972
+
973
+ var sim = d3.forceSimulation()
974
+ .force("link", d3.forceLink().id(function (d) { return d.id; }).distance(140).strength(0.08))
975
+ .force("charge", d3.forceManyBody().strength(-140))
976
+ .force("collide", d3.forceCollide(30).strength(1))
977
+ .force("x", d3.forceX(colX).strength(0.5))
978
+ .force("y", d3.forceY(function (d) { return rowY(d.row); }).strength(0.22));
979
+
980
+ // adjacency for hover highlighting
981
+ var adj = {};
982
+ function computeAdj(links) {
983
+ adj = {};
984
+ links.forEach(function (l) {
985
+ var s = l.source.id || l.source, t = l.target.id || l.target;
986
+ (adj[s] = adj[s] || {})[t] = true;
987
+ (adj[t] = adj[t] || {})[s] = true;
988
+ });
989
+ }
990
+
991
+ var linkSel = linkLayer.selectAll("line");
992
+ var nodeSel = nodeLayer.selectAll("g.node");
993
+ var current = { nodes: [], links: [] };
994
+
995
+ function dims(type) {
996
+ var small = W < 560;
997
+ if (type === "user") return { w: 0, h: 0, r: small ? 27 : 33 };
998
+ if (type === "profile") return { w: small ? 84 : 104, h: 36, r: 0 };
999
+ return { w: small ? 124 : 158, h: 36, r: 0 };
1000
+ }
1001
+
1002
+ function render(mode) {
1003
+ var g = buildGraph(mode);
1004
+ current = g;
1005
+ computeAdj(g.links);
1006
+ stage.setAttribute("data-mode", mode);
1007
+
1008
+ // LINKS
1009
+ linkSel = linkLayer.selectAll("line").data(g.links, function (d) {
1010
+ return (d.source.id || d.source) + ">" + (d.target.id || d.target);
1011
+ });
1012
+ linkSel.exit().remove();
1013
+ linkSel = linkSel.enter().append("line").attr("class", "lnk").merge(linkSel)
1014
+ .attr("class", function (d) { return "lnk" + (mode === "shared" ? " is-shared" : ""); });
1015
+
1016
+ // NODES
1017
+ nodeSel = nodeLayer.selectAll("g.node").data(g.nodes, function (d) { return d.id; });
1018
+ nodeSel.exit().remove();
1019
+ var enter = nodeSel.enter().append("g")
1020
+ .attr("class", function (d) { return "node n-" + d.type; })
1021
+ .call(d3.drag().on("start", dragStart).on("drag", dragged).on("end", dragEnd))
1022
+ .on("mouseenter", hoverOn).on("mouseleave", hoverOff)
1023
+ .on("focus", hoverOn).on("blur", hoverOff)
1024
+ .attr("tabindex", 0);
1025
+
1026
+ enter.each(function (d) {
1027
+ var el = d3.select(this);
1028
+ var dm = dims(d.type);
1029
+ if (d.type === "user") {
1030
+ el.append("circle").attr("class", "ring").attr("r", dm.r + 7);
1031
+ el.append("circle").attr("class", "chip chip-bg").attr("r", dm.r).attr("filter", "url(#glow)");
1032
+ el.append("text").attr("text-anchor", "middle").attr("dy", "0.34em").attr("font-size", "13").text(d.label);
1033
+ } else {
1034
+ el.append("rect").attr("class", "ring").attr("x", -dm.w / 2 - 5).attr("y", -dm.h / 2 - 5)
1035
+ .attr("width", dm.w + 10).attr("height", dm.h + 10).attr("rx", 12);
1036
+ el.append("rect").attr("class", "chip chip-bg").attr("x", -dm.w / 2).attr("y", -dm.h / 2)
1037
+ .attr("width", dm.w).attr("height", dm.h).attr("rx", 10);
1038
+ el.append("text").attr("text-anchor", "middle").attr("dy", "0.34em")
1039
+ .attr("font-size", d.type === "home" ? (W < 560 ? "10" : "11.5") : (W < 560 ? "11.5" : "13")).text(d.label);
1040
+ }
1041
+ var a11y = d.type === "home" ? ("Codex home " + d.label) : (d.type === "profile" ? ("profile " + d.label) : "your macOS user");
1042
+ el.attr("aria-label", a11y).attr("role", "img");
1043
+ });
1044
+ nodeSel = enter.merge(nodeSel);
1045
+
1046
+ sim.nodes(g.nodes);
1047
+ sim.force("link").links(g.links);
1048
+ sim.alpha(0.9).restart();
1049
+ if (reduce) { for (var i = 0; i < 260; i++) sim.tick(); ticked(); sim.alpha(0); }
1050
+
1051
+ hintEl.textContent = mode === "shared"
1052
+ ? "every profile shares one ~/.codex — tokens collide"
1053
+ : "one CODEX_HOME per profile";
1054
+ }
1055
+
1056
+ sim.on("tick", ticked);
1057
+ function ticked() {
1058
+ var pad = 26;
1059
+ current.nodes.forEach(function (d) {
1060
+ d.x = Math.max(pad, Math.min(W - pad, d.x));
1061
+ d.y = Math.max(pad, Math.min(H - pad, d.y));
1062
+ });
1063
+ linkSel
1064
+ .attr("x1", function (d) { return d.source.x; })
1065
+ .attr("y1", function (d) { return d.source.y; })
1066
+ .attr("x2", function (d) { return d.target.x; })
1067
+ .attr("y2", function (d) { return d.target.y; });
1068
+ nodeSel.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; });
1069
+ }
1070
+
1071
+ function dragStart(e, d) { if (!e.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }
1072
+ function dragged(e, d) { d.fx = e.x; d.fy = e.y; }
1073
+ function dragEnd(e, d) { if (!e.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }
1074
+
1075
+ function hoverOn(e, d) {
1076
+ var near = adj[d.id] || {};
1077
+ nodeSel.classed("dim", function (n) { return n.id !== d.id && !near[n.id]; });
1078
+ nodeSel.classed("hot", function (n) { return n.id === d.id; });
1079
+ linkSel.classed("dim", function (l) {
1080
+ var s = l.source.id, t = l.target.id; return !((s === d.id || t === d.id)); });
1081
+ linkSel.classed("hot", function (l) {
1082
+ var s = l.source.id, t = l.target.id; return (s === d.id || t === d.id); });
1083
+ showTip(e, d);
1084
+ }
1085
+ function hoverOff() {
1086
+ nodeSel.classed("dim", false).classed("hot", false);
1087
+ linkSel.classed("dim", false).classed("hot", false);
1088
+ tipEl.classList.remove("show");
1089
+ }
1090
+ function showTip(e, d) {
1091
+ var html;
1092
+ if (d.type === "user") { html = "<b>you</b><br>your macOS user — one set of SSH keys, browser, and GitHub auth"; }
1093
+ else if (d.type === "profile") {
1094
+ var home = (stage.getAttribute("data-mode") === "shared") ? "~/.codex (shared)" : ("~/.codex" + (d.id === "personal" ? "" : "-" + d.id));
1095
+ html = "<b>" + d.label + "</b> &rarr; <b>" + home + "</b><div class='files'>" + FILES + "</div>";
1096
+ } else {
1097
+ html = "<b>" + d.label + "</b><div class='files'>" + FILES + "</div>";
1098
+ }
1099
+ tipEl.innerHTML = html;
1100
+ var sx = W ? (stage.clientWidth / W) : 1, sy = H ? (stage.clientHeight / H) : 1;
1101
+ var px = d.x * sx + 16, py = d.y * sy + 16;
1102
+ if (px > stage.clientWidth - 240) px = d.x * sx - 230;
1103
+ tipEl.style.left = Math.max(8, px) + "px";
1104
+ tipEl.style.top = Math.max(8, py) + "px";
1105
+ tipEl.classList.add("show");
1106
+ }
1107
+
1108
+ // mode toggle
1109
+ var btnIso = document.getElementById("mode-isolated");
1110
+ var btnShared = document.getElementById("mode-shared");
1111
+ function setMode(mode) {
1112
+ btnIso.setAttribute("aria-pressed", String(mode === "isolated"));
1113
+ btnShared.setAttribute("aria-pressed", String(mode === "shared"));
1114
+ render(mode);
1115
+ }
1116
+ btnIso.addEventListener("click", function () { setMode("isolated"); });
1117
+ btnShared.addEventListener("click", function () { setMode("shared"); });
1118
+
1119
+ // responsive: keep viewBox synced to container size
1120
+ var rt;
1121
+ window.addEventListener("resize", function () {
1122
+ clearTimeout(rt);
1123
+ rt = setTimeout(function () {
1124
+ W = stage.clientWidth || W; H = stage.clientHeight || H;
1125
+ svg.attr("viewBox", "0 0 " + W + " " + H);
1126
+ sim.force("x", d3.forceX(colX).strength(0.5));
1127
+ sim.force("y", d3.forceY(function (d) { return rowY(d.row); }).strength(0.22));
1128
+ sim.alpha(0.5).restart();
1129
+ }, 200);
1130
+ });
1131
+
1132
+ render("isolated");
1133
+ })();
1134
+ </script>
1135
+ </body>
1136
+ </html>