luciazero 2.5.0 → 2.5.1

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
@@ -215,15 +215,31 @@ This installs the 13 skills: no doctrine, reviewer, or hooks.
215
215
  <details>
216
216
  <summary><strong>Classic install · Claude Code or Codex CLI</strong></summary>
217
217
 
218
+ Install the command globally once, without `sudo`:
219
+
218
220
  ```bash
219
- npx luciazero # Claude Code
220
- npx luciazero --with-hooks # Claude Code + hooks/statusline; needs Python 3.9+
221
- npx luciazero codex # Codex CLI
221
+ npx luciazero@latest global-install
222
+ ```
223
+
224
+ This installs the CLI under `~/.local/npm` and, after confirmation, adds its
225
+ bin directory to your zsh or bash PATH. Start a new shell, then use the global
226
+ command from any directory:
222
227
 
223
- npx luciazero uninstall
224
- npx luciazero uninstall-codex
228
+ ```bash
229
+ luciazero # Claude Code
230
+ luciazero --with-hooks # Claude Code + hooks/statusline; needs Python 3.9+
231
+ luciazero codex # Codex CLI
232
+
233
+ luciazero uninstall # remove the Claude classic files
234
+ luciazero uninstall-codex # remove the Codex classic files
235
+ luciazero global-status # check the global command and PATH
236
+ luciazero global-uninstall # remove the command and its exact PATH block
225
237
  ```
226
238
 
239
+ For a one-off install without keeping the command, `npx luciazero@latest`
240
+ continues to work. Automation may pass `global-install --yes`; interactive use
241
+ asks before installing the package and changing a shell startup file.
242
+
227
243
  Pick either plugin or classic for Claude Code so hooks are not wired twice.
228
244
  Classic installs support `--status`; Codex receives the doctrine and skills but
229
245
  not Claude-only hooks/statusline. Installers back up name collisions and remove
@@ -236,10 +252,13 @@ only exact Luciazero-managed copies on uninstall.
236
252
  Luciazero never changes classic or Codex files in the background.
237
253
 
238
254
  ```bash
239
- npx luciazero@latest check-update # read-only; contacts npm only now
240
- npx luciazero@latest update # updates every detected classic/Codex install
255
+ luciazero check-update # read-only; contacts npm only now
256
+ luciazero update # updates every detected classic/Codex install
241
257
  ```
242
258
 
259
+ If you chose the one-off path, use the same commands through
260
+ `npx luciazero@latest` instead.
261
+
243
262
  `update` preserves whether the Claude classic install uses hooks, repairs stale
244
263
  managed files, starts no fresh install when it cannot find one, and stops on a
245
264
  known newer version or malformed version metadata. Start a new agent session
package/bin/global.js ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("node:fs");
5
+ const os = require("node:os");
6
+ const path = require("node:path");
7
+ const readline = require("node:readline");
8
+ const { spawnSync } = require("node:child_process");
9
+
10
+ const START = "# luciazero:start global-npm-path";
11
+ const END = "# luciazero:end global-npm-path";
12
+ const BODY = `${START}\nexport PATH="$HOME/.local/npm/bin:$PATH"\n${END}\n`;
13
+
14
+ function locations(env = process.env) {
15
+ const home = env.HOME || os.homedir();
16
+ if (!path.isAbsolute(home)) throw new Error("HOME must be an absolute path");
17
+ const shell = path.basename(env.SHELL || "");
18
+ const rcName = shell === "zsh" ? ".zshrc" : shell === "bash" ? ".bashrc" : null;
19
+ if (!rcName) throw new Error("supported shells are zsh and bash; set SHELL to the shell whose PATH should be updated");
20
+ return { home, prefix: path.join(home, ".local", "npm"), rc: path.join(home, rcName) };
21
+ }
22
+
23
+ function readRc(file) {
24
+ try {
25
+ const stat = fs.lstatSync(file);
26
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${file} is not a regular file; left untouched`);
27
+ return { text: fs.readFileSync(file, "utf8"), mode: stat.mode & 0o777 };
28
+ } catch (error) {
29
+ if (error.code === "ENOENT") return { text: "", mode: 0o600 };
30
+ throw error;
31
+ }
32
+ }
33
+
34
+ function nextRc(current, remove = false) {
35
+ const starts = current.split(START).length - 1;
36
+ const ends = current.split(END).length - 1;
37
+ if (starts !== ends || starts > 1) throw new Error("shell config has malformed Luciazero PATH markers; left untouched");
38
+ if (starts === 1) {
39
+ const begin = current.indexOf(START);
40
+ const finish = current.indexOf("\n", current.indexOf(END, begin));
41
+ const owned = current.slice(begin, finish < 0 ? current.length : finish + 1);
42
+ if (owned !== BODY) throw new Error("shell config has a customized Luciazero PATH block; left untouched");
43
+ if (remove) return current.slice(0, begin) + current.slice(begin + owned.length);
44
+ return current;
45
+ }
46
+ if (remove) return current;
47
+ const separator = current.length && !current.endsWith("\n") ? "\n" : "";
48
+ return current + separator + BODY;
49
+ }
50
+
51
+ function writeRc(file, text, mode) {
52
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
53
+ const tmp = path.join(path.dirname(file), `.${path.basename(file)}.luciazero-${process.pid}-${Date.now()}`);
54
+ const fd = fs.openSync(tmp, "wx", mode);
55
+ try {
56
+ // open(2)'s requested mode is filtered by umask. This is a replacement
57
+ // for an existing user file, so restore its exact permission bits before
58
+ // publishing the temporary file with rename(2).
59
+ fs.fchmodSync(fd, mode);
60
+ fs.writeFileSync(fd, text, "utf8");
61
+ fs.fsyncSync(fd);
62
+ fs.closeSync(fd);
63
+ fs.renameSync(tmp, file);
64
+ } catch (error) {
65
+ try { fs.closeSync(fd); } catch {}
66
+ try { fs.unlinkSync(tmp); } catch {}
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ function npm(args, env = process.env) {
72
+ const result = spawnSync("npm", args, { stdio: "inherit", env });
73
+ if (result.error) throw new Error(`could not run npm: ${result.error.message}`);
74
+ if (result.status !== 0) throw new Error(`npm exited ${result.status === null ? "without a status" : result.status}`);
75
+ }
76
+
77
+ function confirm(question) {
78
+ if (!process.stdin.isTTY) return Promise.resolve(false);
79
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
80
+ return new Promise((resolve) => rl.question(`${question} [y/N] `, (answer) => {
81
+ rl.close();
82
+ resolve(/^y(?:es)?$/i.test(answer.trim()));
83
+ }));
84
+ }
85
+
86
+ async function install(args) {
87
+ if (args.includes("--help")) {
88
+ console.log("Usage: luciazero global-install [--yes]\nInstalls luciazero@latest under ~/.local/npm and adds its bin to PATH.");
89
+ return 0;
90
+ }
91
+ const unknown = args.filter((arg) => arg !== "--yes");
92
+ if (unknown.length) throw new Error(`unknown option: ${unknown[0]}`);
93
+ const place = locations();
94
+ const before = readRc(place.rc);
95
+ const after = nextRc(before.text);
96
+ if (!args.includes("--yes") && !await confirm(`Install luciazero@latest globally in ${place.prefix}?`)) {
97
+ console.error("global install cancelled; nothing changed");
98
+ return 1;
99
+ }
100
+ npm(["install", "--global", "--prefix", place.prefix, "luciazero@latest"]);
101
+ if (after !== before.text) {
102
+ try {
103
+ writeRc(place.rc, after, before.mode);
104
+ } catch (error) {
105
+ throw new Error(
106
+ `the package was installed, but PATH was not changed: ${error.message}. ` +
107
+ `Run ${path.join(place.prefix, "bin", "luciazero")} directly or retry global-install`
108
+ );
109
+ }
110
+ }
111
+ console.log(`luciazero installed globally in ${place.prefix}`);
112
+ console.log(`PATH recorded in ${place.rc}; start a new shell or source that file`);
113
+ return 0;
114
+ }
115
+
116
+ function status(args) {
117
+ if (args.includes("--help")) {
118
+ console.log("Usage: luciazero global-status\nChecks the user-owned global command and its shell PATH block.");
119
+ return 0;
120
+ }
121
+ if (args.length) throw new Error(`unknown option: ${args[0]}`);
122
+ const place = locations();
123
+ const command = path.join(place.prefix, "bin", "luciazero");
124
+ let commandOk = false;
125
+ try {
126
+ const commandStat = fs.statSync(command);
127
+ commandOk = commandStat.isFile() && Boolean(commandStat.mode & 0o111);
128
+ } catch {}
129
+ let pathOk = false;
130
+ try {
131
+ const current = readRc(place.rc).text;
132
+ pathOk = nextRc(current) === current;
133
+ } catch (error) {
134
+ console.error(`luciazero: ${error.message}`);
135
+ return 1;
136
+ }
137
+ if (!commandOk || !pathOk) {
138
+ if (!commandOk) console.error(`MISS ${command}`);
139
+ if (!pathOk) console.error(`MISS Luciazero PATH block in ${place.rc}`);
140
+ return 1;
141
+ }
142
+ console.log(`luciazero is installed globally in ${place.prefix}`);
143
+ console.log(`PATH is recorded in ${place.rc}`);
144
+ return 0;
145
+ }
146
+
147
+ async function uninstall(args) {
148
+ if (args.includes("--help")) {
149
+ console.log("Usage: luciazero global-uninstall [--yes]\nRemoves the global npm package and only Luciazero's exact PATH block.");
150
+ return 0;
151
+ }
152
+ const unknown = args.filter((arg) => arg !== "--yes");
153
+ if (unknown.length) throw new Error(`unknown option: ${unknown[0]}`);
154
+ const place = locations();
155
+ const before = readRc(place.rc);
156
+ const after = nextRc(before.text, true);
157
+ if (!args.includes("--yes") && !await confirm(`Uninstall global luciazero from ${place.prefix}?`)) {
158
+ console.error("global uninstall cancelled; nothing changed");
159
+ return 1;
160
+ }
161
+ npm(["uninstall", "--global", "--prefix", place.prefix, "luciazero"]);
162
+ if (after !== before.text) {
163
+ try {
164
+ writeRc(place.rc, after, before.mode);
165
+ } catch (error) {
166
+ throw new Error(
167
+ `the package was removed, but its PATH block remains in ${place.rc}: ${error.message}. ` +
168
+ `Remove only the lines from '${START}' through '${END}'`
169
+ );
170
+ }
171
+ }
172
+ console.log(`global luciazero removed from ${place.prefix}`);
173
+ return 0;
174
+ }
175
+
176
+ async function main(argv) {
177
+ const [command, ...args] = argv;
178
+ if (command === "install") return install(args);
179
+ if (command === "status") return status(args);
180
+ if (command === "uninstall") return uninstall(args);
181
+ throw new Error(`${command || "command"} is not implemented`);
182
+ }
183
+
184
+ if (require.main === module) {
185
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; }).catch((error) => {
186
+ console.error(`luciazero: ${error.message}`);
187
+ process.exitCode = 1;
188
+ });
189
+ }
190
+
191
+ module.exports = { BODY, locations, nextRc, readRc, writeRc };
package/bin/luciazero.js CHANGED
@@ -9,6 +9,7 @@
9
9
  // npx luciazero discipline [options] -> local stats report
10
10
  // npx luciazero check-update [--json] -> explicit npm version check
11
11
  // npx luciazero update -> update detected classic installs
12
+ // npx luciazero global-install [--yes] -> persistent user-owned CLI
12
13
  // npx luciazero bus status [--json] -> Agent Bus queue summary (beta)
13
14
  const { spawnSync } = require("node:child_process");
14
15
  const path = require("node:path");
@@ -21,6 +22,9 @@ const ROUTES = {
21
22
  discipline: { runtime: process.execPath, script: "bin/discipline-report.js" },
22
23
  "check-update": { runtime: process.execPath, script: "bin/update.js", args: ["check"] },
23
24
  update: { runtime: process.execPath, script: "bin/update.js", args: ["update"] },
25
+ "global-install": { runtime: process.execPath, script: "bin/global.js", args: ["install"] },
26
+ "global-status": { runtime: process.execPath, script: "bin/global.js", args: ["status"] },
27
+ "global-uninstall": { runtime: process.execPath, script: "bin/global.js", args: ["uninstall"] },
24
28
  bus: { runtime: process.execPath, script: "bin/bus.js" },
25
29
  };
26
30
 
@@ -30,7 +34,7 @@ if (args[0] && !args[0].startsWith("-")) {
30
34
  if (!Object.prototype.hasOwnProperty.call(ROUTES, args[0])) {
31
35
  console.error(
32
36
  `luciazero: unknown command '${args[0]}' ` +
33
- "(install, codex, discipline, check-update, update, bus, uninstall, uninstall-codex)"
37
+ "(install, codex, discipline, check-update, update, global-install, global-status, global-uninstall, bus, uninstall, uninstall-codex)"
34
38
  );
35
39
  process.exit(64);
36
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luciazero",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "Verification-first discipline for coding agents (Claude Code + Codex CLI): 9-rule doctrine, 13 skills, risk-routed reviewer, fail-open enforcement hooks. npx luciazero installs it.",
5
5
  "repository": {
6
6
  "type": "git",