trymeanwhile 1.0.0 → 1.2.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 +110 -39
  2. package/package.json +1 -1
package/bin/meanwhile.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // Node reimplementation of install.sh/install.ps1 -- one script instead of
3
- // two, since Node itself is already cross-platform. Behavior is kept in
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
4
5
  // lockstep with those scripts (same install dir, same settings.json shape,
5
- // same non-fatal certifi step) so all three installers produce an
6
- // identical result regardless of which one someone happens to run.
6
+ // same non-fatal certifi step) so every installer produces an identical
7
+ // result regardless of which one someone happens to run.
7
8
 
8
9
  const fs = require("fs");
9
10
  const os = require("os");
@@ -19,10 +20,13 @@ function log(msg) {
19
20
  console.log(`meanwhile: ${msg}`);
20
21
  }
21
22
 
23
+ function commandExists(cmd) {
24
+ return spawnSync(cmd, ["--version"], { stdio: "ignore" }).status === 0;
25
+ }
26
+
22
27
  function findPython() {
23
28
  for (const candidate of ["python3", "python", "py"]) {
24
- const result = spawnSync(candidate, ["--version"], { stdio: "ignore" });
25
- if (result.status === 0) return candidate;
29
+ if (commandExists(candidate)) return candidate;
26
30
  }
27
31
  return null;
28
32
  }
@@ -35,7 +39,87 @@ function openUrl(url) {
35
39
  exec(cmd, () => {});
36
40
  }
37
41
 
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
+ async function downloadTo(remoteName, localPath) {
59
+ const res = await fetch(`${SERVER}/${remoteName}`);
60
+ if (!res.ok) throw new Error(`couldn't download ${remoteName} (${res.status})`);
61
+ fs.writeFileSync(localPath, await res.text());
62
+ }
63
+
64
+ function wireSettings(settingsPath, python, scriptPath) {
65
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
66
+ const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, "utf8") || "{}") : {};
67
+ settings.statusLine = { type: "command", command: `${python} "${scriptPath}"` };
68
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
69
+ log(`wired into ${settingsPath}`);
70
+ }
71
+
72
+ // Wraps any long-running shell command (npm install, docker build, terraform
73
+ // apply, ...) and shows the same disclosed line the Claude Code/Copilot
74
+ // status lines show -- reusing the exact same /line endpoint and billing
75
+ // mechanism, just from a different trigger than an editor hook. The server
76
+ // only bills a line if it stayed on screen >= BILLABLE_THRESHOLD (10s), so
77
+ // this polls every 15s -- fast enough to feel alive, slow enough that real
78
+ // polls actually bill instead of always resetting the timer on each other.
79
+ async function wrapCommand(args) {
80
+ const cmdArgs = args[0] === "--" ? args.slice(1) : args;
81
+ if (cmdArgs.length === 0) {
82
+ log("usage: npx trymeanwhile wrap -- <command> [args...]");
83
+ log("example: npx trymeanwhile wrap -- npm install");
84
+ process.exit(1);
85
+ }
86
+
87
+ fs.mkdirSync(STATE_DIR, { recursive: true });
88
+ const idPath = path.join(STATE_DIR, "install_id");
89
+ if (!fs.existsSync(idPath)) {
90
+ fs.writeFileSync(idPath, crypto.randomUUID(), { mode: 0o600 });
91
+ }
92
+ const installId = fs.readFileSync(idPath, "utf8").trim();
93
+
94
+ const { spawn } = require("child_process");
95
+ const child = spawn(cmdArgs[0], cmdArgs.slice(1), { stdio: "inherit", shell: process.platform === "win32" });
96
+
97
+ let stopped = false;
98
+ (async function pollLoop() {
99
+ while (!stopped) {
100
+ try {
101
+ const res = await fetch(`${SERVER}/line?id=${installId}&event=cli_wrap`);
102
+ if (res.ok) {
103
+ const data = await res.json();
104
+ process.stderr.write(`\nmeanwhile: ${data.line}\n`);
105
+ }
106
+ } catch (e) {}
107
+ await new Promise((r) => setTimeout(r, 15000));
108
+ }
109
+ })();
110
+
111
+ child.on("exit", (code) => {
112
+ stopped = true;
113
+ process.exit(code === null ? 1 : code);
114
+ });
115
+ }
116
+
38
117
  async function main() {
118
+ if (process.argv[2] === "wrap") {
119
+ await wrapCommand(process.argv.slice(3));
120
+ return;
121
+ }
122
+
39
123
  log(`installing to ${INSTALL_DIR}`);
40
124
  fs.mkdirSync(INSTALL_DIR, { recursive: true });
41
125
  fs.mkdirSync(STATE_DIR, { recursive: true });
@@ -43,55 +127,42 @@ async function main() {
43
127
  // Real install ID, generated on the device the moment an install
44
128
  // actually happens -- not guessed ahead of time by the website. Only
45
129
  // if one doesn't already exist, so re-running this never clobbers an
46
- // existing install's history with a fresh random ID.
130
+ // existing install's history with a fresh random ID. Shared across
131
+ // both integrations below, same as the shell installers.
47
132
  const idPath = path.join(STATE_DIR, "install_id");
48
133
  if (!fs.existsSync(idPath)) {
49
134
  fs.writeFileSync(idPath, crypto.randomUUID(), { mode: 0o600 });
50
135
  }
51
136
  const installId = fs.readFileSync(idPath, "utf8").trim();
52
137
 
53
- const res = await fetch(`${SERVER}/statusline.py`);
54
- if (!res.ok) {
55
- console.error(`meanwhile: couldn't download statusline.py (${res.status}) -- try again in a moment.`);
56
- process.exit(1);
57
- }
58
- fs.writeFileSync(path.join(INSTALL_DIR, "statusline.py"), await res.text());
59
-
60
138
  const python = findPython();
61
139
  if (!python) {
62
140
  log("couldn't find Python on your PATH.");
63
141
  log("install it from https://python.org, then run `npx trymeanwhile` again.");
64
142
  process.exit(1);
65
143
  }
144
+ await ensureCertifi(python);
66
145
 
67
- // certifi is a nice-to-have, not a hard requirement -- statusline.py
68
- // falls back to the system's own certificate store if it's missing.
69
- // Never let a failed pip install here take down the rest of the setup.
70
- const hasCertifi = spawnSync(python, ["-c", "import certifi"], { stdio: "ignore" }).status === 0;
71
- if (!hasCertifi) {
72
- log("installing certifi (helps with HTTPS, not required)...");
73
- const pipAttempts = [
74
- ["-m", "pip", "install", "--quiet", "certifi"],
75
- ["-m", "pip", "install", "--quiet", "--user", "certifi"],
76
- ["-m", "pip", "install", "--quiet", "--break-system-packages", "certifi"],
77
- ];
78
- const installed = pipAttempts.some(
79
- (args) => spawnSync(python, args, { stdio: "ignore" }).status === 0
80
- );
81
- if (!installed) log("couldn't install certifi, continuing without it (statusline.py falls back automatically)");
82
- }
146
+ // Wire whichever tool(s) are actually on this machine, so the same
147
+ // command works everywhere instead of a different one per tool. Claude
148
+ // Code is always wired -- it's the primary target this package was
149
+ // built for -- and Copilot CLI is wired too if it's detected on PATH.
150
+ const wireCopilot = commandExists("copilot");
83
151
 
84
- const scriptPath = path.join(INSTALL_DIR, "statusline.py");
85
- const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
86
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
87
- const settings = fs.existsSync(settingsPath) ? JSON.parse(fs.readFileSync(settingsPath, "utf8") || "{}") : {};
88
- settings.statusLine = { type: "command", command: `${python} "${scriptPath}"` };
89
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
90
- log(`wired into ${settingsPath}`);
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);
155
+
156
+ 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.");
161
+ }
91
162
 
92
- log("installed. Restart Claude Code (close and reopen your terminal) to see it live.");
163
+ log("installed. Restart Claude Code (and Copilot CLI, if wired) to see it live.");
93
164
  log("to check earnings or register a payout email later, run:");
94
- log(` ${python} "${scriptPath}" --claim`);
165
+ log(` ${python} "${claudeScript}" --claim`);
95
166
 
96
167
  // Open the browser straight to the claim page -- same pattern as
97
168
  // `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.0.0",
3
+ "version": "1.2.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"