trymeanwhile 1.1.0 → 1.2.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.
Files changed (2) hide show
  1. package/bin/meanwhile.js +72 -45
  2. package/package.json +1 -1
package/bin/meanwhile.js CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
- // Node reimplementation of install.sh/install.ps1/install_copilot.sh -- one
3
- // script instead of three, since Node itself is already cross-platform and
4
- // can detect which tool(s) are actually installed. Behavior is kept in
5
- // lockstep with those scripts (same install dir, same settings.json shape,
6
- // same non-fatal certifi step) so every installer produces an identical
7
- // result regardless of which one someone happens to run.
2
+ // The `npx trymeanwhile` entry point -- one cross-platform script that
3
+ // detects which tool(s) are actually installed and wires them up. The
4
+ // status line it wires in (statusline.js) runs on Node too, not Python:
5
+ // Node is already guaranteed present since npx can't invoke this script
6
+ // without it, so there's no second runtime to find, verify, or fail on.
8
7
 
9
8
  const fs = require("fs");
10
9
  const os = require("os");
@@ -24,13 +23,6 @@ function commandExists(cmd) {
24
23
  return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
25
24
  }
26
25
 
27
- function findPython() {
28
- for (const candidate of ["python3", "python", "py"]) {
29
- if (commandExists(candidate)) return candidate;
30
- }
31
- return null;
32
- }
33
-
34
26
  function openUrl(url) {
35
27
  const cmd =
36
28
  process.platform === "darwin" ? `open "${url}"`
@@ -39,37 +31,71 @@ function openUrl(url) {
39
31
  exec(cmd, () => {});
40
32
  }
41
33
 
42
- async function ensureCertifi(python) {
43
- const hasCertifi = spawnSync(python, ["-c", "import certifi"], { stdio: "ignore" }).status === 0;
44
- if (hasCertifi) return;
45
- // certifi is a nice-to-have, not a hard requirement -- both status line
46
- // scripts fall back to the system's own certificate store if it's
47
- // missing. Never let a failed pip install here take down the install.
48
- log("installing certifi (helps with HTTPS, not required)...");
49
- const pipAttempts = [
50
- ["-m", "pip", "install", "--quiet", "certifi"],
51
- ["-m", "pip", "install", "--quiet", "--user", "certifi"],
52
- ["-m", "pip", "install", "--quiet", "--break-system-packages", "certifi"],
53
- ];
54
- const installed = pipAttempts.some((args) => spawnSync(python, args, { stdio: "ignore" }).status === 0);
55
- if (!installed) log("couldn't install certifi, continuing without it (falls back automatically)");
56
- }
57
-
58
34
  async function downloadTo(remoteName, localPath) {
59
35
  const res = await fetch(`${SERVER}/${remoteName}`);
60
36
  if (!res.ok) throw new Error(`couldn't download ${remoteName} (${res.status})`);
61
37
  fs.writeFileSync(localPath, await res.text());
62
38
  }
63
39
 
64
- function wireSettings(settingsPath, python, scriptPath) {
40
+ function wireSettings(settingsPath, runtime, scriptPath) {
65
41
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
66
42
  const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, "utf8") || "{}") : {};
67
- settings.statusLine = { type: "command", command: `${python} "${scriptPath}"` };
43
+ settings.statusLine = { type: "command", command: `"${runtime}" "${scriptPath}"` };
68
44
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
69
45
  log(`wired into ${settingsPath}`);
70
46
  }
71
47
 
48
+ // Wraps any long-running shell command (npm install, docker build, terraform
49
+ // apply, ...) and shows the same disclosed line the Claude Code/Copilot
50
+ // status lines show -- reusing the exact same /line endpoint and billing
51
+ // mechanism, just from a different trigger than an editor hook. The server
52
+ // only bills a line if it stayed on screen >= BILLABLE_THRESHOLD (10s), so
53
+ // this polls every 15s -- fast enough to feel alive, slow enough that real
54
+ // polls actually bill instead of always resetting the timer on each other.
55
+ async function wrapCommand(args) {
56
+ const cmdArgs = args[0] === "--" ? args.slice(1) : args;
57
+ if (cmdArgs.length === 0) {
58
+ log("usage: npx trymeanwhile wrap -- <command> [args...]");
59
+ log("example: npx trymeanwhile wrap -- npm install");
60
+ process.exit(1);
61
+ }
62
+
63
+ fs.mkdirSync(STATE_DIR, { recursive: true });
64
+ const idPath = path.join(STATE_DIR, "install_id");
65
+ if (!fs.existsSync(idPath)) {
66
+ fs.writeFileSync(idPath, crypto.randomUUID(), { mode: 0o600 });
67
+ }
68
+ const installId = fs.readFileSync(idPath, "utf8").trim();
69
+
70
+ const { spawn } = require("child_process");
71
+ const child = spawn(cmdArgs[0], cmdArgs.slice(1), { stdio: "inherit", shell: process.platform === "win32" });
72
+
73
+ let stopped = false;
74
+ (async function pollLoop() {
75
+ while (!stopped) {
76
+ try {
77
+ const res = await fetch(`${SERVER}/line?id=${installId}&event=cli_wrap`);
78
+ if (res.ok) {
79
+ const data = await res.json();
80
+ process.stderr.write(`\nmeanwhile: ${data.line}\n`);
81
+ }
82
+ } catch (e) {}
83
+ await new Promise((r) => setTimeout(r, 15000));
84
+ }
85
+ })();
86
+
87
+ child.on("exit", (code) => {
88
+ stopped = true;
89
+ process.exit(code === null ? 1 : code);
90
+ });
91
+ }
92
+
72
93
  async function main() {
94
+ if (process.argv[2] === "wrap") {
95
+ await wrapCommand(process.argv.slice(3));
96
+ return;
97
+ }
98
+
73
99
  log(`installing to ${INSTALL_DIR}`);
74
100
  fs.mkdirSync(INSTALL_DIR, { recursive: true });
75
101
  fs.mkdirSync(STATE_DIR, { recursive: true });
@@ -85,13 +111,14 @@ async function main() {
85
111
  }
86
112
  const installId = fs.readFileSync(idPath, "utf8").trim();
87
113
 
88
- const python = findPython();
89
- if (!python) {
90
- log("couldn't find Python on your PATH.");
91
- log("install it from https://python.org, then run `npx trymeanwhile` again.");
92
- process.exit(1);
93
- }
94
- await ensureCertifi(python);
114
+ // Node, not Python: this script is already running under Node (it can't
115
+ // not be -- npx just invoked it), so there is no second runtime to find
116
+ // or verify. process.execPath is the absolute path to the exact node
117
+ // binary running right now, more robust than trusting a bare `node` on
118
+ // PATH (some distros only ship `nodejs`). This used to hard-require
119
+ // python3/python/py on top of Node and fail the whole install for
120
+ // anyone without it -- a real dead stop, not a cosmetic gap.
121
+ const node = process.execPath;
95
122
 
96
123
  // Wire whichever tool(s) are actually on this machine, so the same
97
124
  // command works everywhere instead of a different one per tool. Claude
@@ -99,20 +126,20 @@ async function main() {
99
126
  // built for -- and Copilot CLI is wired too if it's detected on PATH.
100
127
  const wireCopilot = commandExists("copilot");
101
128
 
102
- const claudeScript = path.join(INSTALL_DIR, "statusline.py");
103
- await downloadTo("statusline.py", claudeScript);
104
- wireSettings(path.join(os.homedir(), ".claude", "settings.json"), python, claudeScript);
129
+ const claudeScript = path.join(INSTALL_DIR, "statusline.js");
130
+ await downloadTo("statusline.js", claudeScript);
131
+ wireSettings(path.join(os.homedir(), ".claude", "settings.json"), node, claudeScript);
105
132
 
106
133
  if (wireCopilot) {
107
- const copilotScript = path.join(INSTALL_DIR, "copilot_statusline.py");
108
- await downloadTo("copilot_statusline.py", copilotScript);
109
- wireSettings(path.join(os.homedir(), ".copilot", "settings.json"), python, copilotScript);
134
+ const copilotScript = path.join(INSTALL_DIR, "copilot_statusline.js");
135
+ await downloadTo("copilot_statusline.js", copilotScript);
136
+ wireSettings(path.join(os.homedir(), ".copilot", "settings.json"), node, copilotScript);
110
137
  log("Copilot CLI detected -- wired that too, same install ID, earnings share one balance.");
111
138
  }
112
139
 
113
140
  log("installed. Restart Claude Code (and Copilot CLI, if wired) to see it live.");
114
141
  log("to check earnings or register a payout email later, run:");
115
- log(` ${python} "${claudeScript}" --claim`);
142
+ log(` "${node}" "${claudeScript}" --claim`);
116
143
 
117
144
  // Open the browser straight to the claim page -- same pattern as
118
145
  // `gh auth login` / `vercel login` / `wrangler login`. This is the only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trymeanwhile",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Get paid while your AI coding agent thinks. Wires a disclosed status line into Claude Code / Copilot CLI.",
5
5
  "bin": {
6
6
  "meanwhile": "bin/meanwhile.js"