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 +26 -7
- package/bin/global.js +191 -0
- package/bin/luciazero.js +5 -1
- package/package.json +1 -1
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
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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.
|
|
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",
|