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.
- package/bin/meanwhile.js +72 -45
- package/package.json +1 -1
package/bin/meanwhile.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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,
|
|
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:
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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.
|
|
103
|
-
await downloadTo("statusline.
|
|
104
|
-
wireSettings(path.join(os.homedir(), ".claude", "settings.json"),
|
|
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.
|
|
108
|
-
await downloadTo("copilot_statusline.
|
|
109
|
-
wireSettings(path.join(os.homedir(), ".copilot", "settings.json"),
|
|
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(` ${
|
|
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