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.
@@ -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, staleness
25
- // or version checking, an uninstall path, and any write into the user's
26
- // .claude/ or CLAUDE.md. Undoing this is `rm -rf` of one directory.
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(`entropy-machines ${PKG.version}: vendored into ${dest}`);
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(`entropy-machines: could not run ${vendoredInit}: ${r.error.message}`);
211
- console.error(' The files ARE vendored. Run it yourself from the repo root.');
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('entropy-machines: the files are vendored, but bin/init did not');
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('entropy-machines: done. Next —');
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
- console.log(` 1. commit what init just wrote: git add ${addPaths}`);
235
- console.log(` 2. point your coding agent at ${path.join(shown, 'docs', 'AGENT-QUICKSTART.md')}`);
236
- console.log(` 3. ${path.join(shown, 'bin', 'serve')} — answer the PRD it opens`);
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(`\nentropy-machines: starting serve from ${loc.shown}/\n`);
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
- echo "init: WARNING — $target was WRITTEN, but bin/tracker could not read the" >&2
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
- echo " Check $project/.entropy-machines/ and $target by hand." >&2
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
- echo " $ENTROPY_MACHINES_HOME/bin/init --force" >&2
211
+ printf " ${CYAN}$ENTROPY_MACHINES_HOME/bin/init --force${RESET}\n" >&2
205
212
  exit 2
206
213
  fi
207
214
 
208
- echo "init: wrote $target"
209
- echo "$prd_note"
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
- echo "init: warning — could not render TRACKER.html; run \`$cmd_prefix/tracker render\`" >&2
235
+ printf "${YELLOW}init: warning — could not render TRACKER.html; run \`${CYAN}$cmd_prefix/tracker render${YELLOW}\`${RESET}\n" >&2
229
236
 
230
- echo "init: tracker is live and empty — \`$cmd_prefix/tracker ready\` answers."
231
- echo "init: next — run \`$cmd_prefix/serve\` and answer the PRD it opens."
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
- try:
694
- server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
695
- except OSError as exc:
696
- if exc.errno == 48 or "Address already in use" in str(exc): # EADDRINUSE
697
- print(
698
- "serve: REFUSED — port %d is already in use.\n"
699
- " Another server (maybe a previous `bin/serve`) already holds it.\n"
700
- " Try a different port: bin/serve %d" % (PORT, PORT + 1),
701
- file=sys.stderr,
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
- print("serve: REFUSED could not bind 127.0.0.1:%d (%s)." % (PORT, exc), file=sys.stderr)
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, PORT), flush=True)
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.2",
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": {