@pinegrovedev/cli 0.1.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.
- package/dist/index.js +286 -0
- package/package.json +23 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/setup.ts
|
|
7
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
8
|
+
import { execSync as execSync2, spawnSync } from "node:child_process";
|
|
9
|
+
import { join as join2 } from "node:path";
|
|
10
|
+
import { confirm } from "@inquirer/prompts";
|
|
11
|
+
|
|
12
|
+
// src/platform.ts
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
function detectPlatform() {
|
|
17
|
+
const os = process.platform === "darwin" ? "darwin" : "linux";
|
|
18
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
19
|
+
throw new Error(`Unsupported platform: ${process.platform}. Pinegrove supports macOS and Linux.`);
|
|
20
|
+
}
|
|
21
|
+
let arch;
|
|
22
|
+
switch (process.arch) {
|
|
23
|
+
case "arm64":
|
|
24
|
+
arch = "aarch64";
|
|
25
|
+
break;
|
|
26
|
+
case "x64":
|
|
27
|
+
arch = "x86_64";
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
throw new Error(`Unsupported architecture: ${process.arch}`);
|
|
31
|
+
}
|
|
32
|
+
return { os, arch, key: `${os}-${arch}` };
|
|
33
|
+
}
|
|
34
|
+
function stateDir(os) {
|
|
35
|
+
if (os === "darwin") {
|
|
36
|
+
return join(homedir(), "Library", "Application Support", "Pinegrove");
|
|
37
|
+
}
|
|
38
|
+
return join(homedir(), ".pinegrove");
|
|
39
|
+
}
|
|
40
|
+
function binDir(os) {
|
|
41
|
+
return join(stateDir(os), "bin");
|
|
42
|
+
}
|
|
43
|
+
function daemonPath(os) {
|
|
44
|
+
return join(binDir(os), "pinegrove-daemon");
|
|
45
|
+
}
|
|
46
|
+
function isDaemonInstalled(os) {
|
|
47
|
+
try {
|
|
48
|
+
const path = daemonPath(os);
|
|
49
|
+
execSync(`test -x "${path}"`, { stdio: "ignore" });
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function daemonVersion(os) {
|
|
56
|
+
try {
|
|
57
|
+
return execSync(`"${daemonPath(os)}" --version`, { encoding: "utf-8" }).trim();
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/log.ts
|
|
64
|
+
var RESET = "\x1B[0m";
|
|
65
|
+
var GREEN = "\x1B[32m";
|
|
66
|
+
var YELLOW = "\x1B[33m";
|
|
67
|
+
var RED = "\x1B[31m";
|
|
68
|
+
var DIM = "\x1B[2m";
|
|
69
|
+
var BOLD = "\x1B[1m";
|
|
70
|
+
function info(msg) {
|
|
71
|
+
console.log(`${GREEN}✓${RESET} ${msg}`);
|
|
72
|
+
}
|
|
73
|
+
function warn(msg) {
|
|
74
|
+
console.log(`${YELLOW}!${RESET} ${msg}`);
|
|
75
|
+
}
|
|
76
|
+
function error(msg) {
|
|
77
|
+
console.error(`${RED}✗${RESET} ${msg}`);
|
|
78
|
+
}
|
|
79
|
+
function step(msg) {
|
|
80
|
+
console.log(`
|
|
81
|
+
${BOLD}${msg}${RESET}`);
|
|
82
|
+
}
|
|
83
|
+
function dim(msg) {
|
|
84
|
+
console.log(`${DIM} ${msg}${RESET}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/commands/setup.ts
|
|
88
|
+
var MANIFEST_URL = "https://updates.pinegrove.dev/daemon/latest.json";
|
|
89
|
+
async function fetchManifest() {
|
|
90
|
+
const res = await fetch(MANIFEST_URL);
|
|
91
|
+
if (!res.ok)
|
|
92
|
+
throw new Error(`Failed to fetch version manifest: HTTP ${res.status}`);
|
|
93
|
+
return res.json();
|
|
94
|
+
}
|
|
95
|
+
function downloadAndExtract(url, destDir) {
|
|
96
|
+
mkdirSync(destDir, { recursive: true });
|
|
97
|
+
const archive = "/tmp/pinegrove-daemon.tar.gz";
|
|
98
|
+
execSync2(`curl -fsSL "${url}" -o "${archive}"`, { stdio: "inherit" });
|
|
99
|
+
execSync2(`tar -xzf "${archive}" -C "${destDir}"`, { stdio: "inherit" });
|
|
100
|
+
const expectedPath = join2(destDir, "pinegrove-daemon");
|
|
101
|
+
if (!existsSync(expectedPath)) {
|
|
102
|
+
const files = readdirSync(destDir);
|
|
103
|
+
const extracted = files.find((f) => f !== "pinegrove-daemon" && !f.startsWith("."));
|
|
104
|
+
if (extracted) {
|
|
105
|
+
execSync2(`mv "${join2(destDir, extracted)}" "${expectedPath}"`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
execSync2(`chmod 755 "${expectedPath}"`);
|
|
109
|
+
execSync2(`rm -f "${archive}"`);
|
|
110
|
+
}
|
|
111
|
+
function registerService(daemon) {
|
|
112
|
+
spawnSync(daemon, ["uninstall"], { stdio: "ignore" });
|
|
113
|
+
const result = spawnSync(daemon, ["install", "--port", "19284"], { stdio: "inherit" });
|
|
114
|
+
if (result.status !== 0) {
|
|
115
|
+
throw new Error("Failed to register daemon service");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function runPrereqs(daemon) {
|
|
119
|
+
const result = spawnSync(daemon, ["prereqs", "install"], { stdio: "inherit" });
|
|
120
|
+
return result.status === 0;
|
|
121
|
+
}
|
|
122
|
+
function runSetup(daemon, opts) {
|
|
123
|
+
const args = ["setup"];
|
|
124
|
+
if (opts.token && opts.name) {
|
|
125
|
+
args.push("--token", opts.token, "--name", opts.name);
|
|
126
|
+
}
|
|
127
|
+
args.push("--api-url", opts.apiUrl);
|
|
128
|
+
const result = spawnSync(daemon, args, { stdio: "inherit" });
|
|
129
|
+
return result.status === 0;
|
|
130
|
+
}
|
|
131
|
+
function waitForDaemon(daemon, timeoutMs = 15000) {
|
|
132
|
+
const deadline = Date.now() + timeoutMs;
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
const result = spawnSync(daemon, ["status"], { encoding: "utf-8", stdio: "pipe" });
|
|
135
|
+
if (result.stdout?.includes("running"))
|
|
136
|
+
return true;
|
|
137
|
+
execSync2("sleep 1");
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
async function setup(opts) {
|
|
142
|
+
const platform = detectPlatform();
|
|
143
|
+
step(`Pinegrove Setup (${platform.os}/${platform.arch})`);
|
|
144
|
+
step("Step 1: Daemon binary");
|
|
145
|
+
const existingVersion = daemonVersion(platform.os);
|
|
146
|
+
let manifest = null;
|
|
147
|
+
try {
|
|
148
|
+
manifest = await fetchManifest();
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (existingVersion) {
|
|
151
|
+
warn(`Could not fetch latest version (${e.message}). Using installed v${existingVersion}.`);
|
|
152
|
+
} else {
|
|
153
|
+
error(`Could not fetch latest version: ${e.message}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (manifest) {
|
|
158
|
+
const platformEntry = manifest.platforms[platform.key];
|
|
159
|
+
if (!platformEntry) {
|
|
160
|
+
error(`No daemon build available for ${platform.key}`);
|
|
161
|
+
dim(`Available platforms: ${Object.keys(manifest.platforms).join(", ")}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
if (existingVersion === manifest.version) {
|
|
165
|
+
info(`Daemon v${existingVersion} is already the latest`);
|
|
166
|
+
} else {
|
|
167
|
+
if (existingVersion) {
|
|
168
|
+
info(`Upgrading daemon v${existingVersion} → v${manifest.version}`);
|
|
169
|
+
} else {
|
|
170
|
+
info(`Installing daemon v${manifest.version}`);
|
|
171
|
+
}
|
|
172
|
+
downloadAndExtract(platformEntry.url, binDir(platform.os));
|
|
173
|
+
info(`Installed to ${binDir(platform.os)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const daemon = daemonPath(platform.os);
|
|
177
|
+
if (!isDaemonInstalled(platform.os)) {
|
|
178
|
+
error("Daemon binary not found after installation");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
step("Step 2: System service");
|
|
182
|
+
try {
|
|
183
|
+
registerService(daemon);
|
|
184
|
+
info("Daemon service registered");
|
|
185
|
+
} catch (e) {
|
|
186
|
+
error(e.message);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
if (!opts.skipPrereqs) {
|
|
190
|
+
step("Step 3: Prerequisites (nix, devbox, bubblewrap)");
|
|
191
|
+
dim("Some installers may ask for sudo password");
|
|
192
|
+
console.log();
|
|
193
|
+
const ok = runPrereqs(daemon);
|
|
194
|
+
if (!ok) {
|
|
195
|
+
warn("Some prerequisites need manual installation (see above)");
|
|
196
|
+
const cont = await confirm({ message: "Continue anyway?" });
|
|
197
|
+
if (!cont)
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
step("Step 3: Prerequisites (skipped)");
|
|
202
|
+
}
|
|
203
|
+
step("Step 4: Starting daemon");
|
|
204
|
+
if (!waitForDaemon(daemon)) {
|
|
205
|
+
warn("Daemon not responding — it may need a manual start");
|
|
206
|
+
dim(`Try: ${daemon} start`);
|
|
207
|
+
} else {
|
|
208
|
+
info("Daemon is running");
|
|
209
|
+
}
|
|
210
|
+
step("Step 5: Connect to your team");
|
|
211
|
+
console.log();
|
|
212
|
+
const setupOk = runSetup(daemon, opts);
|
|
213
|
+
if (!setupOk) {
|
|
214
|
+
error("Setup failed — try running manually:");
|
|
215
|
+
dim(`${daemon} setup`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
step("Setup complete!");
|
|
219
|
+
info("Daemon is installed, provisioned, and connected to your team's mesh");
|
|
220
|
+
dim(`Binary: ${daemon}`);
|
|
221
|
+
dim(`State: ${stateDir(platform.os)}`);
|
|
222
|
+
const bin = binDir(platform.os);
|
|
223
|
+
if (!process.env.PATH?.includes(bin)) {
|
|
224
|
+
console.log();
|
|
225
|
+
warn(`Add to your shell profile: export PATH="${bin}:$PATH"`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/commands/status.ts
|
|
230
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
231
|
+
async function status() {
|
|
232
|
+
const platform = detectPlatform();
|
|
233
|
+
step(`Pinegrove Status (${platform.os}/${platform.arch})`);
|
|
234
|
+
if (!isDaemonInstalled(platform.os)) {
|
|
235
|
+
error("Daemon not installed");
|
|
236
|
+
dim("Run: npx @pinegrove/cli setup");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const version = daemonVersion(platform.os);
|
|
240
|
+
info(`Daemon v${version} installed at ${daemonPath(platform.os)}`);
|
|
241
|
+
const daemon = daemonPath(platform.os);
|
|
242
|
+
const statusResult = spawnSync2(daemon, ["status"], { encoding: "utf-8", stdio: "pipe" });
|
|
243
|
+
if (statusResult.stdout?.includes("running")) {
|
|
244
|
+
info(statusResult.stdout.trim());
|
|
245
|
+
} else {
|
|
246
|
+
warn(statusResult.stdout?.trim() || "Daemon not running");
|
|
247
|
+
}
|
|
248
|
+
console.log();
|
|
249
|
+
spawnSync2(daemon, ["prereqs", "status"], { stdio: "inherit" });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/commands/uninstall.ts
|
|
253
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
254
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
255
|
+
async function uninstall() {
|
|
256
|
+
const platform = detectPlatform();
|
|
257
|
+
if (!isDaemonInstalled(platform.os)) {
|
|
258
|
+
error("Daemon not installed");
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const daemon = daemonPath(platform.os);
|
|
262
|
+
const state = stateDir(platform.os);
|
|
263
|
+
const ok = await confirm2({
|
|
264
|
+
message: `This will stop and uninstall the Pinegrove daemon.
|
|
265
|
+
State directory (${state}) will be preserved.
|
|
266
|
+
Continue?`
|
|
267
|
+
});
|
|
268
|
+
if (!ok)
|
|
269
|
+
return;
|
|
270
|
+
step("Stopping daemon");
|
|
271
|
+
spawnSync3(daemon, ["stop"], { stdio: "inherit" });
|
|
272
|
+
step("Removing service");
|
|
273
|
+
spawnSync3(daemon, ["uninstall"], { stdio: "inherit" });
|
|
274
|
+
info("Daemon uninstalled. State directory preserved at " + state);
|
|
275
|
+
dim("To fully remove: rm -rf " + state);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/index.ts
|
|
279
|
+
var program = new Command().name("pinegrove").description("Pinegrove CLI — install and provision the Pinegrove daemon").version("0.1.0");
|
|
280
|
+
program.command("setup").description("Install the daemon, prerequisites, and connect to your team").option("--api-url <url>", "Pinegrove API base URL", "https://api.pinegrove.dev").option("--skip-prereqs", "Skip prerequisite installation").option("--token <token>", "Provisioning token (headless mode)").option("--name <name>", "Device name (headless mode)").action(setup);
|
|
281
|
+
program.command("status").description("Show daemon and prerequisite status").action(status);
|
|
282
|
+
program.command("uninstall").description("Stop and remove the daemon").action(uninstall);
|
|
283
|
+
if (process.argv.length <= 2) {
|
|
284
|
+
process.argv.push("setup");
|
|
285
|
+
}
|
|
286
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pinegrovedev/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pinegrove": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "bun build src/index.ts --outdir dist --target node --format esm --external commander --external @inquirer/prompts",
|
|
14
|
+
"prepublishOnly": "bun run build"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"commander": "^13",
|
|
18
|
+
"@inquirer/prompts": "^7"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
}
|
|
23
|
+
}
|