claude-dev-env 8.33.2 → 8.35.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/bin/install-pstack-plugin.mjs +65 -33
- package/bin/install-pstack-plugin.test.mjs +93 -17
- package/bin/install.agents-home.test.mjs +1 -0
- package/bin/install.codex-rules.test.mjs +2 -0
- package/bin/install.cursor-rules.test.mjs +1 -0
- package/bin/install.mjs +23 -9
- package/bin/install.prune.test.mjs +1 -0
- package/bin/install.question-presentation.test.mjs +4 -0
- package/bin/install.settings-defaults.test.mjs +1 -0
- package/bin/install.test.mjs +76 -0
- package/bin/install.transaction.test.mjs +8 -0
- package/bin/install.uninstall-transaction.test.mjs +2 -0
- package/docs/codex-accounts.md +65 -12
- package/package.json +1 -1
- package/scripts/claude_account_profile.py +68 -21
- package/scripts/codex_account_choice.py +389 -24
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +65 -2
- package/scripts/test_claude_account_profile.py +34 -0
- package/scripts/test_codex_account_choice.py +369 -1
|
@@ -84,6 +84,7 @@ test('capture and restore recover settings, manifest, files, and hooksPath', ()
|
|
|
84
84
|
const env = {
|
|
85
85
|
...process.env,
|
|
86
86
|
CDE_INSTALL_PSTACK: '0',
|
|
87
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
87
88
|
HOME: box.root,
|
|
88
89
|
USERPROFILE: box.root,
|
|
89
90
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -145,6 +146,7 @@ test('capture and restore recover an additional host settings file', () => {
|
|
|
145
146
|
env: {
|
|
146
147
|
...process.env,
|
|
147
148
|
CDE_INSTALL_PSTACK: '0',
|
|
149
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
148
150
|
HOME: box.root,
|
|
149
151
|
USERPROFILE: box.root,
|
|
150
152
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -179,6 +181,7 @@ test('runWithInstallTransaction restores prior state on injected fault', () => {
|
|
|
179
181
|
const env = {
|
|
180
182
|
...process.env,
|
|
181
183
|
CDE_INSTALL_PSTACK: '0',
|
|
184
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
182
185
|
HOME: box.root,
|
|
183
186
|
USERPROFILE: box.root,
|
|
184
187
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -232,6 +235,7 @@ test('runWithInstallTransaction commits and discards journal on success', () =>
|
|
|
232
235
|
env: {
|
|
233
236
|
...process.env,
|
|
234
237
|
CDE_INSTALL_PSTACK: '0',
|
|
238
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
235
239
|
HOME: box.root,
|
|
236
240
|
USERPROFILE: box.root,
|
|
237
241
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -274,6 +278,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
274
278
|
const childEnvironment = {
|
|
275
279
|
...process.env,
|
|
276
280
|
CDE_INSTALL_PSTACK: '0',
|
|
281
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
277
282
|
HOME: homeDirectory,
|
|
278
283
|
USERPROFILE: homeDirectory,
|
|
279
284
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -308,6 +313,7 @@ test('a later install fault removes newly seeded Codex pstack files', () => {
|
|
|
308
313
|
faultPhase: FAULT_PHASES.AFTER_MANIFEST_WRITE,
|
|
309
314
|
environment: {
|
|
310
315
|
CDE_INSTALL_PSTACK: '1',
|
|
316
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
311
317
|
CDE_CODEX_EXECUTABLE: codexCommandPath,
|
|
312
318
|
},
|
|
313
319
|
});
|
|
@@ -450,6 +456,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
|
|
|
450
456
|
env: {
|
|
451
457
|
...process.env,
|
|
452
458
|
CDE_INSTALL_PSTACK: '0',
|
|
459
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
453
460
|
HOME: homeDirectory,
|
|
454
461
|
USERPROFILE: homeDirectory,
|
|
455
462
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -466,6 +473,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
|
|
|
466
473
|
env: {
|
|
467
474
|
...process.env,
|
|
468
475
|
CDE_INSTALL_PSTACK: '0',
|
|
476
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
469
477
|
HOME: homeDirectory,
|
|
470
478
|
USERPROFILE: homeDirectory,
|
|
471
479
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -72,6 +72,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
72
72
|
const childEnvironment = {
|
|
73
73
|
...process.env,
|
|
74
74
|
CDE_INSTALL_PSTACK: '0',
|
|
75
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
75
76
|
HOME: homeDirectory,
|
|
76
77
|
USERPROFILE: homeDirectory,
|
|
77
78
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -305,6 +306,7 @@ test('uninstall fault after_git_config restores prior core.hooksPath', () => {
|
|
|
305
306
|
env: {
|
|
306
307
|
...process.env,
|
|
307
308
|
CDE_INSTALL_PSTACK: '0',
|
|
309
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
308
310
|
HOME: homeDirectory,
|
|
309
311
|
USERPROFILE: homeDirectory,
|
|
310
312
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
package/docs/codex-accounts.md
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
# Codex accounts
|
|
2
2
|
|
|
3
|
-
The codex_account_choice picker spreads agent work across
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The codex_account_choice picker spreads agent work across the Codex accounts
|
|
4
|
+
on one machine. Each account signs in under its own Codex home, and every home
|
|
5
|
+
shares the same Codex setup: config, rules, skills, plugins, prompts and agents.
|
|
6
|
+
A job asks the picker which account to use.
|
|
7
7
|
|
|
8
8
|
## Pieces
|
|
9
9
|
|
|
10
10
|
| File | What it does |
|
|
11
11
|
|---|---|
|
|
12
|
-
| `scripts/codex_account_choice.py` | `choose` names the account and tier a job runs on, `check` tells a running job whether its account is still above a floor, `sync` links the shared setup into every account's home |
|
|
12
|
+
| `scripts/codex_account_choice.py` | `choose` names the account and tier a job runs on, `check` tells a running job whether its account is still above a floor, `sync` links the shared setup into every account's home, `setup` and `install` write one launcher per account |
|
|
13
13
|
| `scripts/codex_account_meters.py` | Reads one account's rate-limit windows through `codex app-server` with `CODEX_HOME` set to that account's home |
|
|
14
|
-
| `scripts/dev_env_scripts_constants/codex_account_constants.py` | The account names
|
|
14
|
+
| `scripts/dev_env_scripts_constants/codex_account_constants.py` | The fallback account names, the roster variable and file, the launcher template, the shared entry names, the 10% bar, the 1% Luna stop, and the 20% 5-hour floor for Luna |
|
|
15
15
|
|
|
16
16
|
## Names and order
|
|
17
17
|
|
|
18
|
-
The
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
The account roster names the accounts and their order. It comes from the
|
|
19
|
+
`CODEX_ACCOUNT_PROFILES` environment variable, a comma-separated list such as
|
|
20
|
+
`alpha,beta`. When that variable is unset or empty, the roster is the saved list
|
|
21
|
+
in `account-launchers.json` under the profiles root. When neither exists, the
|
|
22
|
+
accounts are `codex-1`, `codex-2`, `codex-3` and `codex-4`.
|
|
23
|
+
|
|
24
|
+
`choose`, `sync` and `check` all work on that list, in that order. `check`
|
|
25
|
+
accepts only a name on it. Each account's home is `~/.codex-profiles/<name>`, or
|
|
26
|
+
`<name>` under `CODEX_PROFILES_ROOT` when set. `~/.codex` holds the shared setup
|
|
27
|
+
and is never an account. A name uses letters, digits, hyphens or underscores.
|
|
28
|
+
`main`, `wait` and the Windows device names are refused.
|
|
22
29
|
|
|
23
30
|
## Sign in once
|
|
24
31
|
|
|
@@ -33,8 +40,54 @@ $env:CODEX_HOME = "$HOME\.codex-profiles\codex-1"; codex login
|
|
|
33
40
|
```
|
|
34
41
|
|
|
35
42
|
The sign-in lives in that folder's `auth.json`. Only the entries in
|
|
36
|
-
`ALL_SHARED_CODEX_HOME_NAMES` link to `~/.codex`.
|
|
37
|
-
|
|
43
|
+
`ALL_SHARED_CODEX_HOME_NAMES` link to `~/.codex`. They are `AGENTS.md`, `agents`,
|
|
44
|
+
`config.toml`, `hooks`, `hooks.json`, `plugins`, `prompts`, `rules` and `skills`.
|
|
45
|
+
Sign-in, sessions, history, logs and state files stay per account.
|
|
46
|
+
|
|
47
|
+
## Named launchers
|
|
48
|
+
|
|
49
|
+
Each roster account gets a launcher, `codex-<name>.cmd`, in `~/.local/bin`. It
|
|
50
|
+
sets `CODEX_HOME` to that account's home and runs Codex with every argument, so
|
|
51
|
+
`codex-alpha exec "fix the test"` runs on the `alpha` account.
|
|
52
|
+
|
|
53
|
+
Run the setup once:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
python packages/claude-dev-env/scripts/codex_account_choice.py setup
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
It prints the saved names, then asks for one name per line. A blank line or the
|
|
60
|
+
end of input finishes. An invalid name prints the reason and asks again. A
|
|
61
|
+
repeated name counts once. The setup saves the list to
|
|
62
|
+
`~/.codex-profiles/account-launchers.json`, links each account's home to
|
|
63
|
+
`~/.codex`, and writes each launcher.
|
|
64
|
+
|
|
65
|
+
Rerun the installer after the shared setup or this package changes:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
python packages/claude-dev-env/scripts/codex_account_choice.py install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It reads the roster, links every account's home, and rewrites any launcher that
|
|
72
|
+
differs. A second run changes nothing. With no roster it prints `{}`.
|
|
73
|
+
`CODEX_ACCOUNT_PROFILES` overrides the saved file here too. Both commands take
|
|
74
|
+
`--main-home` and `--launcher-directory`.
|
|
75
|
+
|
|
76
|
+
The launcher runs `call codex %*`. `codex` is npm's `codex.cmd`, and its last
|
|
77
|
+
line ends the batch scope. Without `call`, that line also drops the launcher's
|
|
78
|
+
`CODEX_HOME`, and Codex runs on the main home.
|
|
79
|
+
|
|
80
|
+
The names typed in one setup run become the whole roster. A saved name
|
|
81
|
+
left out has its launcher renamed to `codex-<name>.cmd.replaced-<time>`. Its
|
|
82
|
+
folder under the profiles root stays, with its `auth.json`, so typing the name
|
|
83
|
+
again restores it. A blank first line empties the roster and moves every launcher
|
|
84
|
+
aside.
|
|
85
|
+
|
|
86
|
+
`check <name>` reads a saved name the same way:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
python packages/claude-dev-env/scripts/codex_account_choice.py check alpha --floor 1
|
|
90
|
+
```
|
|
38
91
|
|
|
39
92
|
## Which account a job uses
|
|
40
93
|
|
package/package.json
CHANGED
|
@@ -309,8 +309,60 @@ def sync_profile(
|
|
|
309
309
|
)
|
|
310
310
|
|
|
311
311
|
|
|
312
|
-
def
|
|
313
|
-
|
|
312
|
+
def sync_report_payload(report: ProfileSyncReport) -> dict[str, object]:
|
|
313
|
+
"""Turn a sync report into its JSON payload.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
report: The entries one sync linked, moved aside, and unlinked.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
The linked, moved-aside, and unlinked entries as JSON lists.
|
|
320
|
+
"""
|
|
321
|
+
return {
|
|
322
|
+
JSON_LINKED_KEY: list(report.all_linked),
|
|
323
|
+
JSON_MOVED_ASIDE_KEY: list(report.all_moved_aside),
|
|
324
|
+
JSON_UNLINKED_KEY: list(report.all_unlinked),
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def move_launcher_aside(launcher_path: Path, now: datetime) -> Path:
|
|
329
|
+
"""Rename a launcher to ``<name>.replaced-<time>`` beside it.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
launcher_path: The launcher to move.
|
|
333
|
+
now: The run time that names the moved launcher.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
The moved launcher's path.
|
|
337
|
+
"""
|
|
338
|
+
moved_path = launcher_path.with_name(
|
|
339
|
+
f"{launcher_path.name}{LAUNCHER_REPLACED_SUFFIX}{_stamp(now)}"
|
|
340
|
+
)
|
|
341
|
+
os.replace(launcher_path, moved_path)
|
|
342
|
+
return moved_path
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def launcher_path(
|
|
346
|
+
launcher_directory: Path,
|
|
347
|
+
profile_name: str,
|
|
348
|
+
launcher_file_name_template: str = LAUNCHER_FILE_NAME_TEMPLATE,
|
|
349
|
+
) -> Path:
|
|
350
|
+
"""Name the launcher file for a profile.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
launcher_directory: The directory on PATH that holds the launcher.
|
|
354
|
+
profile_name: The name used in the launcher file name.
|
|
355
|
+
launcher_file_name_template: The launcher file name, with ``{profile_name}``.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
The launcher path.
|
|
359
|
+
|
|
360
|
+
Raises:
|
|
361
|
+
ValueError: When the profile name is not a valid profile name.
|
|
362
|
+
"""
|
|
363
|
+
return launcher_directory / launcher_file_name_template.format(
|
|
364
|
+
profile_name=validate_profile_name(profile_name)
|
|
365
|
+
)
|
|
314
366
|
|
|
315
367
|
|
|
316
368
|
def write_launcher(
|
|
@@ -319,6 +371,8 @@ def write_launcher(
|
|
|
319
371
|
profile_home: Path,
|
|
320
372
|
now: datetime,
|
|
321
373
|
profile_name: str = SECOND_ACCOUNT_PROFILE_NAME,
|
|
374
|
+
launcher_file_name_template: str = LAUNCHER_FILE_NAME_TEMPLATE,
|
|
375
|
+
launcher_text_template: str = LAUNCHER_TEXT_TEMPLATE,
|
|
322
376
|
) -> Path:
|
|
323
377
|
"""Write the launcher that runs Claude under a named profile.
|
|
324
378
|
|
|
@@ -333,26 +387,24 @@ def write_launcher(
|
|
|
333
387
|
profile_home: The named account's Claude home.
|
|
334
388
|
now: The run time that names a moved older launcher.
|
|
335
389
|
profile_name: The name used in the launcher file name.
|
|
390
|
+
launcher_file_name_template: The launcher file name, with ``{profile_name}``.
|
|
391
|
+
launcher_text_template: The launcher body, with ``{profile_home}``.
|
|
336
392
|
|
|
337
393
|
Returns:
|
|
338
394
|
The launcher path.
|
|
339
395
|
"""
|
|
340
|
-
|
|
341
|
-
profile_name
|
|
396
|
+
target_path = launcher_path(
|
|
397
|
+
launcher_directory, profile_name, launcher_file_name_template
|
|
342
398
|
)
|
|
343
|
-
|
|
344
|
-
launcher_text = LAUNCHER_TEXT_TEMPLATE.format(profile_home=profile_home)
|
|
399
|
+
launcher_text = launcher_text_template.format(profile_home=profile_home)
|
|
345
400
|
launcher_bytes = launcher_text.encode(TEXT_ENCODING)
|
|
346
|
-
if
|
|
347
|
-
return
|
|
348
|
-
if
|
|
349
|
-
|
|
350
|
-
launcher_path,
|
|
351
|
-
launcher_path.with_name(_moved_launcher_name(launcher_file_name, now)),
|
|
352
|
-
)
|
|
401
|
+
if target_path.is_file() and target_path.read_bytes() == launcher_bytes:
|
|
402
|
+
return target_path
|
|
403
|
+
if target_path.is_file():
|
|
404
|
+
move_launcher_aside(target_path, now)
|
|
353
405
|
launcher_directory.mkdir(parents=True, exist_ok=True)
|
|
354
|
-
|
|
355
|
-
return
|
|
406
|
+
target_path.write_bytes(launcher_bytes)
|
|
407
|
+
return target_path
|
|
356
408
|
|
|
357
409
|
|
|
358
410
|
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
@@ -403,12 +455,7 @@ def _sync_named_profile(arguments: argparse.Namespace) -> dict[str, object]:
|
|
|
403
455
|
now=now,
|
|
404
456
|
profile_name=arguments.profile_name,
|
|
405
457
|
)
|
|
406
|
-
return {
|
|
407
|
-
JSON_LINKED_KEY: list(report.all_linked),
|
|
408
|
-
JSON_MOVED_ASIDE_KEY: list(report.all_moved_aside),
|
|
409
|
-
JSON_UNLINKED_KEY: list(report.all_unlinked),
|
|
410
|
-
JSON_LAUNCHER_KEY: str(launcher_path),
|
|
411
|
-
}
|
|
458
|
+
return {**sync_report_payload(report), JSON_LAUNCHER_KEY: str(launcher_path)}
|
|
412
459
|
|
|
413
460
|
|
|
414
461
|
if __name__ == "__main__":
|