trymeanwhile 1.2.0 → 1.3.0

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 +87 -51
  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");
@@ -16,19 +15,24 @@ const SERVER = "https://trymeanwhile.online";
16
15
  const INSTALL_DIR = path.join(os.homedir(), ".deadtime-client");
17
16
  const STATE_DIR = path.join(os.homedir(), ".deadtime");
18
17
 
18
+ const VERBOSE = process.argv.includes("--verbose");
19
+
19
20
  function log(msg) {
20
21
  console.log(`meanwhile: ${msg}`);
21
22
  }
22
23
 
23
- function commandExists(cmd) {
24
- return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
24
+ // Wiring detail nobody needs to see on a normal run -- which settings.json,
25
+ // which script path, that kind of thing. Real information, just not the
26
+ // first-run experience: it used to be seven identical-looking lines with no
27
+ // hierarchy, so the one line that actually mattered (restart your tool)
28
+ // read the same as housekeeping noise. Still here for anyone debugging why
29
+ // it didn't wire correctly -- just behind --verbose instead of always on.
30
+ function vlog(msg) {
31
+ if (VERBOSE) log(msg);
25
32
  }
26
33
 
27
- function findPython() {
28
- for (const candidate of ["python3", "python", "py"]) {
29
- if (commandExists(candidate)) return candidate;
30
- }
31
- return null;
34
+ function commandExists(cmd) {
35
+ return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
32
36
  }
33
37
 
34
38
  function openUrl(url) {
@@ -39,34 +43,18 @@ function openUrl(url) {
39
43
  exec(cmd, () => {});
40
44
  }
41
45
 
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
46
  async function downloadTo(remoteName, localPath) {
59
47
  const res = await fetch(`${SERVER}/${remoteName}`);
60
48
  if (!res.ok) throw new Error(`couldn't download ${remoteName} (${res.status})`);
61
49
  fs.writeFileSync(localPath, await res.text());
62
50
  }
63
51
 
64
- function wireSettings(settingsPath, python, scriptPath) {
52
+ function wireSettings(settingsPath, runtime, scriptPath) {
65
53
  fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
66
54
  const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, "utf8") || "{}") : {};
67
- settings.statusLine = { type: "command", command: `${python} "${scriptPath}"` };
55
+ settings.statusLine = { type: "command", command: `"${runtime}" "${scriptPath}"` };
68
56
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
69
- log(`wired into ${settingsPath}`);
57
+ vlog(`wired into ${settingsPath}`);
70
58
  }
71
59
 
72
60
  // Wraps any long-running shell command (npm install, docker build, terraform
@@ -114,13 +102,49 @@ async function wrapCommand(args) {
114
102
  });
115
103
  }
116
104
 
105
+ // `npx trymeanwhile claim` -- the whole point of this is that nobody
106
+ // should ever need to remember (or read) a raw node/path/--claim
107
+ // incantation just to check what they've earned. Same install ID file,
108
+ // same /earnings endpoint statusline.js already hits, just reachable
109
+ // through the one command name people already typed once.
110
+ async function claimCommand() {
111
+ const idPath = path.join(STATE_DIR, "install_id");
112
+ if (!fs.existsSync(idPath)) {
113
+ log("nothing to claim yet -- you haven't installed. run `npx trymeanwhile` first.");
114
+ process.exit(1);
115
+ }
116
+ const installId = fs.readFileSync(idPath, "utf8").trim();
117
+ const claimUrl = `${SERVER}/claim?id=${installId}`;
118
+
119
+ console.log("meanwhile -- your account");
120
+ console.log(` ID: ${installId}`);
121
+ try {
122
+ const res = await fetch(`${SERVER}/earnings?id=${installId}`);
123
+ if (res.ok) {
124
+ const earnings = await res.json();
125
+ console.log(` earned: $${Number(earnings.user_earnings).toFixed(2)}`);
126
+ console.log(` shown: ${earnings.total_calls} lines (${earnings.sponsor_calls} sponsored)`);
127
+ } else {
128
+ console.log(" earned: (couldn't reach server -- check your connection)");
129
+ }
130
+ } catch {
131
+ console.log(" earned: (couldn't reach server -- check your connection)");
132
+ }
133
+ console.log();
134
+ console.log(` register a payout email: ${claimUrl}`);
135
+ }
136
+
117
137
  async function main() {
118
138
  if (process.argv[2] === "wrap") {
119
139
  await wrapCommand(process.argv.slice(3));
120
140
  return;
121
141
  }
142
+ if (process.argv[2] === "claim") {
143
+ await claimCommand();
144
+ return;
145
+ }
122
146
 
123
- log(`installing to ${INSTALL_DIR}`);
147
+ vlog(`installing to ${INSTALL_DIR}`);
124
148
  fs.mkdirSync(INSTALL_DIR, { recursive: true });
125
149
  fs.mkdirSync(STATE_DIR, { recursive: true });
126
150
 
@@ -135,13 +159,14 @@ async function main() {
135
159
  }
136
160
  const installId = fs.readFileSync(idPath, "utf8").trim();
137
161
 
138
- const python = findPython();
139
- if (!python) {
140
- log("couldn't find Python on your PATH.");
141
- log("install it from https://python.org, then run `npx trymeanwhile` again.");
142
- process.exit(1);
143
- }
144
- await ensureCertifi(python);
162
+ // Node, not Python: this script is already running under Node (it can't
163
+ // not be -- npx just invoked it), so there is no second runtime to find
164
+ // or verify. process.execPath is the absolute path to the exact node
165
+ // binary running right now, more robust than trusting a bare `node` on
166
+ // PATH (some distros only ship `nodejs`). This used to hard-require
167
+ // python3/python/py on top of Node and fail the whole install for
168
+ // anyone without it -- a real dead stop, not a cosmetic gap.
169
+ const node = process.execPath;
145
170
 
146
171
  // Wire whichever tool(s) are actually on this machine, so the same
147
172
  // command works everywhere instead of a different one per tool. Claude
@@ -149,20 +174,31 @@ async function main() {
149
174
  // built for -- and Copilot CLI is wired too if it's detected on PATH.
150
175
  const wireCopilot = commandExists("copilot");
151
176
 
152
- const claudeScript = path.join(INSTALL_DIR, "statusline.py");
153
- await downloadTo("statusline.py", claudeScript);
154
- wireSettings(path.join(os.homedir(), ".claude", "settings.json"), python, claudeScript);
177
+ const claudeScript = path.join(INSTALL_DIR, "statusline.js");
178
+ await downloadTo("statusline.js", claudeScript);
179
+ wireSettings(path.join(os.homedir(), ".claude", "settings.json"), node, claudeScript);
155
180
 
156
181
  if (wireCopilot) {
157
- const copilotScript = path.join(INSTALL_DIR, "copilot_statusline.py");
158
- await downloadTo("copilot_statusline.py", copilotScript);
159
- wireSettings(path.join(os.homedir(), ".copilot", "settings.json"), python, copilotScript);
160
- log("Copilot CLI detected -- wired that too, same install ID, earnings share one balance.");
182
+ const copilotScript = path.join(INSTALL_DIR, "copilot_statusline.js");
183
+ await downloadTo("copilot_statusline.js", copilotScript);
184
+ wireSettings(path.join(os.homedir(), ".copilot", "settings.json"), node, copilotScript);
185
+ vlog("Copilot CLI detected -- wired that too, same install ID, earnings share one balance.");
161
186
  }
162
187
 
163
- log("installed. Restart Claude Code (and Copilot CLI, if wired) to see it live.");
164
- log("to check earnings or register a payout email later, run:");
165
- log(` ${python} "${claudeScript}" --claim`);
188
+ // The one-time summary a real person actually reads. This used to be
189
+ // seven identically-styled lines -- the one thing that actually matters
190
+ // (restart your tool) read no differently than housekeeping. Blank
191
+ // lines and indentation do the hierarchy work no --verbose flag can.
192
+ console.log("meanwhile: installed. congratulations, you now get paid to wait.");
193
+ console.log();
194
+ console.log(" restart Claude Code to see it live");
195
+ console.log(" (the one step between you and money -- yes, actually do it)");
196
+ console.log();
197
+ if (wireCopilot) {
198
+ console.log("Copilot CLI's in on this too, if it's here. Same balance either way.");
199
+ console.log();
200
+ }
201
+ console.log("Check what you've earned: npx trymeanwhile claim");
166
202
 
167
203
  // Open the browser straight to the claim page -- same pattern as
168
204
  // `gh auth login` / `vercel login` / `wrangler login`. This is the only
@@ -171,7 +207,7 @@ async function main() {
171
207
  // because someone merely copied a command.
172
208
  const claimUrl = `${SERVER}/claim.html?id=${installId}`;
173
209
  openUrl(claimUrl);
174
- log(`if a browser tab didn't open: ${claimUrl}`);
210
+ console.log(`(if a browser tab didn't open: ${claimUrl})`);
175
211
  }
176
212
 
177
213
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trymeanwhile",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
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"