entropy-machines 0.1.2 → 0.1.4
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/entropy-machines-init +66 -16
- package/bin/init +15 -8
- package/bin/serve +25 -12
- package/package.json +1 -1
|
@@ -21,9 +21,15 @@
|
|
|
21
21
|
// against a clone does).
|
|
22
22
|
//
|
|
23
23
|
// Deliberately NOT here, because the owner deleted a 1072-line installer to
|
|
24
|
-
// get rid of them: managed blocks, hash fences, install receipts,
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// get rid of them: managed blocks, hash fences, install receipts, an
|
|
25
|
+
// uninstall path, and any write into the user's .claude/ or CLAUDE.md.
|
|
26
|
+
// Undoing this is `rm -rf` of one directory.
|
|
27
|
+
//
|
|
28
|
+
// VERSION STAMPING is the one piece that came back: a .version file in the
|
|
29
|
+
// vendored directory lets `start` detect a stale copy and auto-update the
|
|
30
|
+
// harness code (bin/, lib/, etc.) without re-running bin/init or touching
|
|
31
|
+
// config.json. Without it, `npx entropy-machines start` after a version bump
|
|
32
|
+
// silently ran the old code and the user had to know to delete and re-vendor.
|
|
27
33
|
|
|
28
34
|
const fs = require('node:fs');
|
|
29
35
|
const path = require('node:path');
|
|
@@ -47,6 +53,15 @@ const NPM_ENTRY = path.basename(__filename);
|
|
|
47
53
|
|
|
48
54
|
const DEFAULT_DIR = 'entropy-machines';
|
|
49
55
|
|
|
56
|
+
// ANSI color, gated on tty + NO_COLOR (https://no-color.org/).
|
|
57
|
+
const USE_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
58
|
+
const BOLD = USE_COLOR ? '\x1b[1m' : '';
|
|
59
|
+
const DIM = USE_COLOR ? '\x1b[2m' : '';
|
|
60
|
+
const GREEN = USE_COLOR ? '\x1b[32m' : '';
|
|
61
|
+
const YELLOW = USE_COLOR ? '\x1b[33m' : '';
|
|
62
|
+
const CYAN = USE_COLOR ? '\x1b[36m' : '';
|
|
63
|
+
const RESET = USE_COLOR ? '\x1b[0m' : '';
|
|
64
|
+
|
|
50
65
|
function die(lines) {
|
|
51
66
|
for (const line of [].concat(lines)) console.error(line);
|
|
52
67
|
process.exit(2);
|
|
@@ -195,32 +210,35 @@ function init(argv) {
|
|
|
195
210
|
fs.copyFileSync(path.join(PKG_ROOT, 'LICENSE'), licenseDst);
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
// Stamp the version so `start` can detect a stale vendored copy.
|
|
214
|
+
fs.writeFileSync(path.join(dest, '.version'), PKG.version + '\n');
|
|
215
|
+
|
|
198
216
|
const shown = path.relative(root, dest) || '.';
|
|
199
|
-
console.log(
|
|
200
|
-
for (const d of HARNESS_DIRS) console.log(` ${path.join(shown, d)}
|
|
201
|
-
console.log(` ${path.join(shown, 'LICENSE')}${licenseNote}`);
|
|
217
|
+
console.log(`${GREEN}entropy-machines ${PKG.version}: vendored into ${dest}${RESET}`);
|
|
218
|
+
for (const d of HARNESS_DIRS) console.log(` ${DIM}${path.join(shown, d)}/${RESET}`);
|
|
219
|
+
console.log(` ${DIM}${path.join(shown, 'LICENSE')}${licenseNote}${RESET}`);
|
|
202
220
|
console.log('');
|
|
203
221
|
|
|
204
222
|
// ---- Hand over to the harness. Everything past here is shell + Python. ----
|
|
205
223
|
|
|
206
224
|
const vendoredInit = path.join(dest, 'bin', 'init');
|
|
207
|
-
console.log(`entropy-machines: running ${path.join(shown, 'bin', 'init')}`);
|
|
225
|
+
console.log(`entropy-machines: running ${DIM}${path.join(shown, 'bin', 'init')}${RESET}`);
|
|
208
226
|
const r = spawnSync(vendoredInit, [], { cwd: root, stdio: 'inherit' });
|
|
209
227
|
if (r.error) {
|
|
210
|
-
console.error(
|
|
211
|
-
console.error(
|
|
228
|
+
console.error(`${YELLOW}entropy-machines: could not run ${vendoredInit}: ${r.error.message}${RESET}`);
|
|
229
|
+
console.error(` The files ARE vendored. Run it yourself from the repo root.`);
|
|
212
230
|
process.exit(2);
|
|
213
231
|
}
|
|
214
232
|
if (r.status !== 0) {
|
|
215
233
|
console.error('');
|
|
216
|
-
console.error(
|
|
234
|
+
console.error(`${YELLOW}entropy-machines: the files are vendored, but bin/init did not${RESET}`);
|
|
217
235
|
console.error(' finish. Fix what it named above and re-run it from the repo');
|
|
218
|
-
console.error(` root: ${path.join(shown, 'bin', 'init')}`);
|
|
236
|
+
console.error(` root: ${CYAN}${path.join(shown, 'bin', 'init')}${RESET}`);
|
|
219
237
|
process.exit(r.status === null ? 2 : r.status);
|
|
220
238
|
}
|
|
221
239
|
|
|
222
240
|
console.log('');
|
|
223
|
-
console.log(
|
|
241
|
+
console.log(`${BOLD}entropy-machines: done. Next —${RESET}`);
|
|
224
242
|
// Named paths only, never a guessed docs directory — that one is config
|
|
225
243
|
// (docs.dir), and CONFIG.md rule 1 makes a hardcoded path in bin/ a bug.
|
|
226
244
|
// NEVER `git add .` for the root layout. This command is reached over npm,
|
|
@@ -231,9 +249,12 @@ function init(argv) {
|
|
|
231
249
|
shown === '.'
|
|
232
250
|
? `${HARNESS_DIRS.join(' ')} LICENSE .gitignore`
|
|
233
251
|
: `${shown} .gitignore`;
|
|
234
|
-
|
|
235
|
-
console.log(`
|
|
236
|
-
|
|
252
|
+
// Step 1: agent context (most important — the agent needs this first)
|
|
253
|
+
console.log(` ${BOLD}1. point your coding agent at ${CYAN}${path.join(shown, 'docs', 'AGENT-QUICKSTART.md')}${RESET}`);
|
|
254
|
+
// Step 2: run serve to get the PRD
|
|
255
|
+
console.log(` 2. ${CYAN}${path.join(shown, 'bin', 'serve')}${RESET} — answer the PRD it opens (creates issues)`);
|
|
256
|
+
// Step 3: commit (least urgent)
|
|
257
|
+
console.log(` ${DIM}3. commit what init wrote: git add ${addPaths}${RESET}`);
|
|
237
258
|
}
|
|
238
259
|
|
|
239
260
|
function findHarness(root) {
|
|
@@ -245,6 +266,22 @@ function findHarness(root) {
|
|
|
245
266
|
return null;
|
|
246
267
|
}
|
|
247
268
|
|
|
269
|
+
// Re-copy harness code (bin/, lib/, etc.) without re-running bin/init or
|
|
270
|
+
// touching config.json. Called by start() when the vendored .version is older
|
|
271
|
+
// than the running package.
|
|
272
|
+
function updateHarness(loc) {
|
|
273
|
+
const dest = loc.dir;
|
|
274
|
+
for (const d of HARNESS_DIRS) {
|
|
275
|
+
fs.cpSync(path.join(PKG_ROOT, d), path.join(dest, d), {
|
|
276
|
+
recursive: true,
|
|
277
|
+
filter: copyFilter,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// LICENSE: overwrite — it's ours, not user-edited.
|
|
281
|
+
fs.copyFileSync(path.join(PKG_ROOT, 'LICENSE'), path.join(dest, 'LICENSE'));
|
|
282
|
+
fs.writeFileSync(path.join(dest, '.version'), PKG.version + '\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
248
285
|
function start(argv) {
|
|
249
286
|
const root = repoRoot();
|
|
250
287
|
if (!root) die(['entropy-machines: not inside a git repository.']);
|
|
@@ -258,11 +295,24 @@ function start(argv) {
|
|
|
258
295
|
if (!loc) {
|
|
259
296
|
die(['entropy-machines: config.json not found at root or in ' + DEFAULT_DIR + '/.']);
|
|
260
297
|
}
|
|
298
|
+
|
|
299
|
+
// AUTO-UPDATE: if the vendored copy is older than this package, re-copy
|
|
300
|
+
// the harness code. config.json and the docs directory are untouched.
|
|
301
|
+
const versionFile = path.join(loc.dir, '.version');
|
|
302
|
+
let vendored = '';
|
|
303
|
+
try { vendored = fs.readFileSync(versionFile, 'utf8').trim(); } catch {}
|
|
304
|
+
if (vendored !== PKG.version) {
|
|
305
|
+
const from = vendored || 'unknown';
|
|
306
|
+
console.log(`${GREEN}entropy-machines: updating ${loc.shown}/ from ${from} → ${PKG.version}${RESET}`);
|
|
307
|
+
updateHarness(loc);
|
|
308
|
+
console.log(`${DIM} (config.json and docs left alone)${RESET}\n`);
|
|
309
|
+
}
|
|
310
|
+
|
|
261
311
|
const serve = path.join(loc.dir, 'bin', 'serve');
|
|
262
312
|
if (!fs.existsSync(serve)) {
|
|
263
313
|
die([`entropy-machines: ${path.join(loc.shown, 'bin', 'serve')} not found.`]);
|
|
264
314
|
}
|
|
265
|
-
console.log(`\
|
|
315
|
+
console.log(`\n${BOLD}entropy-machines: starting serve from ${loc.shown}/${RESET}\n`);
|
|
266
316
|
const r = spawnSync('sh', [serve], { cwd: loc.dir, stdio: 'inherit' });
|
|
267
317
|
process.exit(r.status ?? 1);
|
|
268
318
|
}
|
package/bin/init
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
# --force overwrite an existing config.json.
|
|
19
19
|
set -e
|
|
20
20
|
|
|
21
|
+
# ---- ANSI color, gated on tty + NO_COLOR (https://no-color.org/) ----
|
|
22
|
+
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
|
|
23
|
+
BOLD='\033[1m' DIM='\033[2m' GREEN='\033[32m' YELLOW='\033[33m' CYAN='\033[36m' RESET='\033[0m'
|
|
24
|
+
else
|
|
25
|
+
BOLD='' DIM='' GREEN='' YELLOW='' CYAN='' RESET=''
|
|
26
|
+
fi
|
|
27
|
+
|
|
21
28
|
usage() {
|
|
22
29
|
cat >&2 <<'EOF'
|
|
23
30
|
usage: bin/init [--force]
|
|
@@ -195,18 +202,18 @@ fi
|
|
|
195
202
|
# in fact everything was — and a plain re-run then died on "config.json
|
|
196
203
|
# already exists" with no repair path named.
|
|
197
204
|
if ! (cd "$project" && "$ENTROPY_MACHINES_HOME/bin/tracker" ready >/dev/null 2>&1); then
|
|
198
|
-
|
|
205
|
+
printf "${YELLOW}init: WARNING — $target was WRITTEN, but bin/tracker could not read the${RESET}\n" >&2
|
|
199
206
|
echo " tracker back afterward. This is not a refusal: everything above" >&2
|
|
200
207
|
echo " really was written, and the project is half-initialised." >&2
|
|
201
|
-
|
|
208
|
+
printf " Check ${DIM}$project/.entropy-machines/${RESET} and ${DIM}$target${RESET} by hand.\n" >&2
|
|
202
209
|
echo " To re-run once you have fixed it (a plain re-run will refuse," >&2
|
|
203
210
|
echo " because config.json now exists):" >&2
|
|
204
|
-
|
|
211
|
+
printf " ${CYAN}$ENTROPY_MACHINES_HOME/bin/init --force${RESET}\n" >&2
|
|
205
212
|
exit 2
|
|
206
213
|
fi
|
|
207
214
|
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
printf "${GREEN}init: wrote $target${RESET}\n"
|
|
216
|
+
printf "${GREEN}$prd_note${RESET}\n"
|
|
210
217
|
if [ -n "$prd_written" ]; then
|
|
211
218
|
echo " questions in it are the ones only you can answer."
|
|
212
219
|
fi
|
|
@@ -225,8 +232,8 @@ esac
|
|
|
225
232
|
# the first document the harness hands them would otherwise hit a 404 — which
|
|
226
233
|
# is exactly the bug that made this necessary.
|
|
227
234
|
"$ENTROPY_MACHINES_HOME/bin/tracker" render >/dev/null 2>&1 || \
|
|
228
|
-
|
|
235
|
+
printf "${YELLOW}init: warning — could not render TRACKER.html; run \`${CYAN}$cmd_prefix/tracker render${YELLOW}\`${RESET}\n" >&2
|
|
229
236
|
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
printf "${GREEN}init: tracker is live and empty${RESET} — \`${CYAN}$cmd_prefix/tracker ready${RESET}\` answers.\n"
|
|
238
|
+
printf "${BOLD}init: next — run \`${CYAN}$cmd_prefix/serve${RESET}${BOLD}\` and answer the PRD it opens.${RESET}\n"
|
|
232
239
|
echo " Filing the issues it produces is step one."
|
package/bin/serve
CHANGED
|
@@ -690,18 +690,31 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
690
690
|
|
|
691
691
|
|
|
692
692
|
def main():
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
693
|
+
# TRY THE REQUESTED PORT, THEN SCAN UPWARD. A user running `npx
|
|
694
|
+
# entropy-machines start` for the first time should not be stopped by a
|
|
695
|
+
# stale process on 8787 — the server picks the next open port and says
|
|
696
|
+
# which one it got. Cap at 20 attempts so a misconfigured machine gets a
|
|
697
|
+
# clear error, not an infinite loop.
|
|
698
|
+
MAX_ATTEMPTS = 20
|
|
699
|
+
port = PORT
|
|
700
|
+
server = None
|
|
701
|
+
for attempt in range(MAX_ATTEMPTS):
|
|
702
|
+
try:
|
|
703
|
+
server = ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
704
|
+
break
|
|
705
|
+
except OSError as exc:
|
|
706
|
+
if exc.errno == 48 or "Address already in use" in str(exc):
|
|
707
|
+
if attempt == 0 and port == PORT:
|
|
708
|
+
print("serve: port %d busy, scanning for an open port…" % port,
|
|
709
|
+
file=sys.stderr, flush=True)
|
|
710
|
+
port += 1
|
|
711
|
+
continue
|
|
712
|
+
print("serve: REFUSED — could not bind 127.0.0.1:%d (%s)." % (port, exc),
|
|
713
|
+
file=sys.stderr)
|
|
703
714
|
sys.exit(1)
|
|
704
|
-
|
|
715
|
+
if server is None:
|
|
716
|
+
print("serve: REFUSED — ports %d–%d all busy." % (PORT, PORT + MAX_ATTEMPTS - 1),
|
|
717
|
+
file=sys.stderr)
|
|
705
718
|
sys.exit(1)
|
|
706
719
|
# flush=True IS THE POINT OF THESE TWO LINES. Python block-buffers stdout
|
|
707
720
|
# when it is not a tty — which is exactly the case a bootstrapping agent
|
|
@@ -710,7 +723,7 @@ def main():
|
|
|
710
723
|
# never drained, because the very next statement blocks in serve_forever()
|
|
711
724
|
# forever; the log stays 0 bytes while the server runs and 0 bytes after
|
|
712
725
|
# it is killed. The URL is the one thing this command exists to produce.
|
|
713
|
-
print("serving %s on http://localhost:%d" % (ENTROPY_MACHINES_ROOT,
|
|
726
|
+
print("serving %s on http://localhost:%d" % (ENTROPY_MACHINES_ROOT, port), flush=True)
|
|
714
727
|
print(" docs: %s (%s)" % (DOCS_PATH, DOCS_DIR), flush=True)
|
|
715
728
|
print(" theme: %s (%s)" % (THEME_NAME, THEME_FILE), flush=True)
|
|
716
729
|
try:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "entropy-machines",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Vendors the entropy-machines agent-orchestration harness into your git repository as plain tracked files. A delivery mechanism, not a runtime — the harness is POSIX shell and Python 3 and needs no Node once the files have landed.",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"repository": {
|