@pimote/pimote 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/LICENSE +21 -0
- package/README.md +333 -0
- package/bin/pimote.js +8 -0
- package/client/build/_app/env.js +1 -0
- package/client/build/_app/immutable/assets/0.CsjXJ2oE.css +2 -0
- package/client/build/_app/immutable/assets/2.CIRqqeIr.css +1 -0
- package/client/build/_app/immutable/assets/inter-cyrillic-ext-wght-normal.BOeWTOD4.woff2 +0 -0
- package/client/build/_app/immutable/assets/inter-cyrillic-wght-normal.DqGufNeO.woff2 +0 -0
- package/client/build/_app/immutable/assets/inter-greek-ext-wght-normal.DlzME5K_.woff2 +0 -0
- package/client/build/_app/immutable/assets/inter-greek-wght-normal.CkhJZR-_.woff2 +0 -0
- package/client/build/_app/immutable/assets/inter-latin-ext-wght-normal.DO1Apj_S.woff2 +0 -0
- package/client/build/_app/immutable/assets/inter-latin-wght-normal.Dx4kXJAl.woff2 +0 -0
- package/client/build/_app/immutable/assets/inter-vietnamese-wght-normal.CBcvBZtf.woff2 +0 -0
- package/client/build/_app/immutable/assets/jetbrains-mono-cyrillic-wght-normal.D73BlboJ.woff2 +0 -0
- package/client/build/_app/immutable/assets/jetbrains-mono-greek-wght-normal.Bw9x6K1M.woff2 +0 -0
- package/client/build/_app/immutable/assets/jetbrains-mono-latin-ext-wght-normal.DBQx-q_a.woff2 +0 -0
- package/client/build/_app/immutable/assets/jetbrains-mono-latin-wght-normal.B9CIFXIH.woff2 +0 -0
- package/client/build/_app/immutable/assets/jetbrains-mono-vietnamese-wght-normal.Bt-aOZkq.woff2 +0 -0
- package/client/build/_app/immutable/chunks/5FogVG_p.js +1 -0
- package/client/build/_app/immutable/chunks/BN18Mjoo.js +1 -0
- package/client/build/_app/immutable/chunks/BTSGQ0LP.js +3 -0
- package/client/build/_app/immutable/chunks/BTW4yCoz.js +1 -0
- package/client/build/_app/immutable/chunks/BgJ-X-tf.js +3 -0
- package/client/build/_app/immutable/chunks/CHncfsjL.js +1 -0
- package/client/build/_app/immutable/chunks/CnTTbAN2.js +1 -0
- package/client/build/_app/immutable/chunks/CnuZs6QA.js +1 -0
- package/client/build/_app/immutable/chunks/CvWR-ThL.js +1 -0
- package/client/build/_app/immutable/chunks/D1hYfEew.js +1 -0
- package/client/build/_app/immutable/chunks/D5m3x_L9.js +5 -0
- package/client/build/_app/immutable/chunks/L5t1qIFa.js +50 -0
- package/client/build/_app/immutable/entry/app.BjHwmkZK.js +2 -0
- package/client/build/_app/immutable/entry/start.CZeUhs5D.js +1 -0
- package/client/build/_app/immutable/nodes/0.HHf1ps7Y.js +5 -0
- package/client/build/_app/immutable/nodes/1.CjbUSBAL.js +1 -0
- package/client/build/_app/immutable/nodes/2.C22f_gRz.js +49 -0
- package/client/build/_app/version.json +1 -0
- package/client/build/index.html +45 -0
- package/client/build/pwa/badge-96.png +0 -0
- package/client/build/pwa/icon-192.png +0 -0
- package/client/build/pwa/icon-512.png +0 -0
- package/client/build/pwa/manifest.json +39 -0
- package/client/build/robots.txt +3 -0
- package/client/build/sw.js +2 -0
- package/package.json +81 -0
- package/patches/@mariozechner+pi-coding-agent+0.65.0.patch +24 -0
- package/scripts/postinstall-patches.mjs +55 -0
- package/server/dist/cli.js +347 -0
- package/server/dist/config.js +78 -0
- package/server/dist/event-buffer.js +223 -0
- package/server/dist/extension-ui-bridge.js +175 -0
- package/server/dist/folder-index.js +126 -0
- package/server/dist/index.js +54 -0
- package/server/dist/message-mapper.js +80 -0
- package/server/dist/panel-state.js +28 -0
- package/server/dist/paths.js +14 -0
- package/server/dist/push-infrastructure.js +73 -0
- package/server/dist/push-notification.js +56 -0
- package/server/dist/server.js +223 -0
- package/server/dist/session-manager.js +313 -0
- package/server/dist/session-metadata.js +81 -0
- package/server/dist/takeover.js +172 -0
- package/server/dist/ws-handler.js +989 -0
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pimote/pimote",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Web client and embedded server for pi with multi-session browser access, streaming, and extension UI support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Antoine Lennartz <alennartz@gmail.com>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/alennartz/pimote.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/alennartz/pimote#readme",
|
|
13
|
+
"bugs": "https://github.com/alennartz/pimote/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi",
|
|
16
|
+
"pimote",
|
|
17
|
+
"coding-agent",
|
|
18
|
+
"browser",
|
|
19
|
+
"pwa",
|
|
20
|
+
"remote"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"pimote": "bin/pimote.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/pimote.js",
|
|
30
|
+
"client/build/**",
|
|
31
|
+
"server/dist/**/*.js",
|
|
32
|
+
"scripts/postinstall-patches.mjs",
|
|
33
|
+
"patches/@mariozechner+pi-coding-agent+0.65.0.patch",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"workspaces": [
|
|
41
|
+
"shared",
|
|
42
|
+
"server",
|
|
43
|
+
"client",
|
|
44
|
+
"packages/panels"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "npm run build --workspace=shared && npm run build --workspace=@pimote/server && npm run build --workspace=client",
|
|
48
|
+
"start": "node ./bin/pimote.js start",
|
|
49
|
+
"format": "prettier --write .",
|
|
50
|
+
"format:check": "prettier --check .",
|
|
51
|
+
"lint": "eslint .",
|
|
52
|
+
"check": "npm run check --workspace=client && tsc --noEmit -p server/tsconfig.json && tsc --noEmit -p shared/tsconfig.json",
|
|
53
|
+
"prepare": "husky",
|
|
54
|
+
"postinstall": "node ./scripts/postinstall-patches.mjs",
|
|
55
|
+
"prepack": "npm run build",
|
|
56
|
+
"pack:dry-run": "npm pack --dry-run"
|
|
57
|
+
},
|
|
58
|
+
"lint-staged": {
|
|
59
|
+
"*.{ts,js,svelte,css,html,json,md,yaml}": "prettier --write",
|
|
60
|
+
"*.{ts,js,svelte}": "eslint --no-fix"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@eslint/js": "^10.0.1",
|
|
64
|
+
"eslint": "^10.2.0",
|
|
65
|
+
"eslint-plugin-svelte": "^3.17.0",
|
|
66
|
+
"globals": "^17.4.0",
|
|
67
|
+
"husky": "^9.1.7",
|
|
68
|
+
"lint-staged": "^16.4.0",
|
|
69
|
+
"prettier": "^3.8.1",
|
|
70
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
71
|
+
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
72
|
+
"typescript-eslint": "^8.58.0"
|
|
73
|
+
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
|
76
|
+
"@mariozechner/pi-coding-agent": "0.65.0",
|
|
77
|
+
"patch-package": "^8.0.1",
|
|
78
|
+
"web-push": "^3.6.7",
|
|
79
|
+
"ws": "^8.20.0"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
diff --git a/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session-runtime.js b/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session-runtime.js
|
|
2
|
+
index 9ab786e..6f89829 100644
|
|
3
|
+
--- a/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session-runtime.js
|
|
4
|
+
+++ b/node_modules/@mariozechner/pi-coding-agent/dist/core/agent-session-runtime.js
|
|
5
|
+
@@ -74,9 +74,6 @@ export class AgentSessionRuntime {
|
|
6
|
+
this.session.dispose();
|
|
7
|
+
}
|
|
8
|
+
apply(result) {
|
|
9
|
+
- if (process.cwd() !== result.services.cwd) {
|
|
10
|
+
- process.chdir(result.services.cwd);
|
|
11
|
+
- }
|
|
12
|
+
this._session = result.session;
|
|
13
|
+
this._services = result.services;
|
|
14
|
+
this._diagnostics = result.diagnostics;
|
|
15
|
+
@@ -223,9 +220,6 @@ export class AgentSessionRuntime {
|
|
16
|
+
*/
|
|
17
|
+
export async function createAgentSessionRuntime(createRuntime, options) {
|
|
18
|
+
const result = await createRuntime(options);
|
|
19
|
+
- if (process.cwd() !== result.services.cwd) {
|
|
20
|
+
- process.chdir(result.services.cwd);
|
|
21
|
+
- }
|
|
22
|
+
return new AgentSessionRuntime(result.session, result.services, createRuntime, result.diagnostics, result.modelFallbackMessage);
|
|
23
|
+
}
|
|
24
|
+
export { createAgentSessionFromServices, createAgentSessionServices, } from "./agent-session-services.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { dirname, join, relative } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const pathParts = packageRoot.split('/node_modules/');
|
|
10
|
+
const installRoot = pathParts.length > 1 ? pathParts[0] : packageRoot;
|
|
11
|
+
const patchDir = join(packageRoot, 'patches');
|
|
12
|
+
const patchPackageEntrypoint = join(installRoot, 'node_modules', 'patch-package', 'index.js');
|
|
13
|
+
|
|
14
|
+
async function hasPatchFiles(dir) {
|
|
15
|
+
try {
|
|
16
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
17
|
+
return entries.some((entry) => entry.isFile() && entry.name.endsWith('.patch'));
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function exists(path) {
|
|
24
|
+
try {
|
|
25
|
+
await stat(path);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function main() {
|
|
33
|
+
if (!(await hasPatchFiles(patchDir))) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!(await exists(patchPackageEntrypoint))) {
|
|
38
|
+
throw new Error(`[pimote] Could not find patch-package at ${patchPackageEntrypoint}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const patchDirArg = relative(installRoot, patchDir) || patchDir;
|
|
42
|
+
const result = spawnSync(process.execPath, [patchPackageEntrypoint, '--patch-dir', patchDirArg], {
|
|
43
|
+
cwd: installRoot,
|
|
44
|
+
stdio: 'inherit',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (result.status !== 0) {
|
|
48
|
+
process.exit(result.status ?? 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main().catch((err) => {
|
|
53
|
+
console.error(err instanceof Error ? err.message : err);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { access, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { CONFIG_PATH } from './config.js';
|
|
8
|
+
import { PIMOTE_STATE_DIR } from './paths.js';
|
|
9
|
+
import { main as startPimote } from './index.js';
|
|
10
|
+
const ROOT_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
11
|
+
export async function getVersion() {
|
|
12
|
+
const raw = await readFile(join(ROOT_DIR, 'package.json'), 'utf-8');
|
|
13
|
+
const pkg = JSON.parse(raw);
|
|
14
|
+
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
15
|
+
}
|
|
16
|
+
export function printHelp() {
|
|
17
|
+
console.log(`pimote — browser UI for pi
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
pimote [--port <port>] Start Pimote (runs first-time setup if needed)
|
|
21
|
+
pimote start [--port <port>] Start Pimote using your existing config
|
|
22
|
+
pimote init [options] Create or update Pimote config
|
|
23
|
+
pimote help Show this help
|
|
24
|
+
pimote version Show the installed version
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-p, --port <port> Override the server port for this run, or set it during init
|
|
28
|
+
-r, --root <path> Add a project root during init (repeatable)
|
|
29
|
+
-h, --help Show this help
|
|
30
|
+
-v, --version Show the installed version
|
|
31
|
+
|
|
32
|
+
Environment:
|
|
33
|
+
PORT Override the configured server port
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
pimote
|
|
37
|
+
pimote --port 3001
|
|
38
|
+
pimote init --root ~/projects --root ~/work --port 3001
|
|
39
|
+
`);
|
|
40
|
+
}
|
|
41
|
+
export function expandHomePath(value) {
|
|
42
|
+
if (value === '~')
|
|
43
|
+
return homedir();
|
|
44
|
+
if (value.startsWith('~/'))
|
|
45
|
+
return join(homedir(), value.slice(2));
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
export async function pathExists(path) {
|
|
49
|
+
try {
|
|
50
|
+
await access(path);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function isDirectory(path) {
|
|
58
|
+
try {
|
|
59
|
+
const info = await stat(path);
|
|
60
|
+
return info.isDirectory();
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function looksLikeProjectDirectory(path) {
|
|
67
|
+
return (await pathExists(join(path, '.git'))) || (await pathExists(join(path, 'package.json')));
|
|
68
|
+
}
|
|
69
|
+
export function parsePort(raw) {
|
|
70
|
+
const text = String(raw).trim();
|
|
71
|
+
if (!/^\d+$/.test(text)) {
|
|
72
|
+
throw new Error(`Invalid port: ${raw}`);
|
|
73
|
+
}
|
|
74
|
+
const port = Number.parseInt(text, 10);
|
|
75
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
76
|
+
throw new Error(`Invalid port: ${raw}`);
|
|
77
|
+
}
|
|
78
|
+
return port;
|
|
79
|
+
}
|
|
80
|
+
export async function normalizeRoots(values) {
|
|
81
|
+
const parts = values
|
|
82
|
+
.flatMap((value) => String(value).split(','))
|
|
83
|
+
.map((value) => value.trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
if (parts.length === 0) {
|
|
86
|
+
throw new Error('At least one project root is required.');
|
|
87
|
+
}
|
|
88
|
+
const uniqueRoots = [];
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
for (const part of parts) {
|
|
91
|
+
const resolved = resolve(expandHomePath(part));
|
|
92
|
+
if (!(await isDirectory(resolved))) {
|
|
93
|
+
throw new Error(`Project root does not exist or is not a directory: ${part}`);
|
|
94
|
+
}
|
|
95
|
+
if (!seen.has(resolved)) {
|
|
96
|
+
seen.add(resolved);
|
|
97
|
+
uniqueRoots.push(resolved);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return uniqueRoots;
|
|
101
|
+
}
|
|
102
|
+
async function suggestDefaultRoot() {
|
|
103
|
+
const cwd = process.cwd();
|
|
104
|
+
if (await looksLikeProjectDirectory(cwd)) {
|
|
105
|
+
return dirname(cwd);
|
|
106
|
+
}
|
|
107
|
+
const candidates = [join(homedir(), 'projects'), join(homedir(), 'work'), join(homedir(), 'repos')];
|
|
108
|
+
for (const candidate of candidates) {
|
|
109
|
+
if (await isDirectory(candidate))
|
|
110
|
+
return candidate;
|
|
111
|
+
}
|
|
112
|
+
return cwd;
|
|
113
|
+
}
|
|
114
|
+
function isPlainObject(value) {
|
|
115
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
116
|
+
}
|
|
117
|
+
async function readExistingConfigObject(configPath) {
|
|
118
|
+
try {
|
|
119
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
120
|
+
const parsed = JSON.parse(raw);
|
|
121
|
+
if (!isPlainObject(parsed)) {
|
|
122
|
+
return {
|
|
123
|
+
exists: true,
|
|
124
|
+
data: {},
|
|
125
|
+
warning: `[pimote] Existing config at ${configPath} is not a JSON object. It will be replaced.`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return { exists: true, data: parsed };
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
|
|
132
|
+
return { exists: false, data: {} };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
exists: true,
|
|
136
|
+
data: {},
|
|
137
|
+
warning: `[pimote] Existing config at ${configPath} could not be read. It will be replaced.`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function buildConfigPayload(existingConfig, roots, port) {
|
|
142
|
+
return {
|
|
143
|
+
...existingConfig,
|
|
144
|
+
roots,
|
|
145
|
+
port,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function writeConfig(configPath, config) {
|
|
149
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
150
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
151
|
+
}
|
|
152
|
+
function printSetupIntro(configPath, stateDir, isUpdate) {
|
|
153
|
+
console.log(isUpdate ? '[pimote] Updating Pimote setup.' : '[pimote] Welcome to Pimote.');
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log('Pimote runs a local web app for pi. It scans the project roots you choose,');
|
|
156
|
+
console.log('starts a local server, and opens your pi sessions in the browser.');
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log('Before you start chatting, make sure pi has at least one working provider/model');
|
|
159
|
+
console.log('configured, either through API keys or your existing pi login setup.');
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(`Config file: ${configPath}`);
|
|
162
|
+
console.log(`State dir: ${stateDir}`);
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
|
165
|
+
async function promptWithDefault(rl, label, defaultValue) {
|
|
166
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
167
|
+
const answer = await rl.question(`${label}${suffix}: `);
|
|
168
|
+
return answer.trim() || defaultValue;
|
|
169
|
+
}
|
|
170
|
+
async function promptForRoots(rl, defaultRoots) {
|
|
171
|
+
while (true) {
|
|
172
|
+
const answer = await promptWithDefault(rl, 'Project roots (comma-separated parent directories to scan for repos)', defaultRoots.join(', '));
|
|
173
|
+
try {
|
|
174
|
+
return await normalizeRoots([answer]);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
console.error(`[pimote] ${err instanceof Error ? err.message : err}`);
|
|
178
|
+
console.error('[pimote] Example: ~/projects, ~/work');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function promptForPort(rl, defaultPort) {
|
|
183
|
+
while (true) {
|
|
184
|
+
const answer = await promptWithDefault(rl, 'Port', String(defaultPort));
|
|
185
|
+
try {
|
|
186
|
+
return parsePort(answer);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
console.error(`[pimote] ${err instanceof Error ? err.message : err}`);
|
|
190
|
+
console.error('[pimote] Enter a number between 1 and 65535.');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function runInteractiveInit(options) {
|
|
195
|
+
const { configPath, stateDir, existingConfig, cliPort, cliRoots } = options;
|
|
196
|
+
const existingRoots = Array.isArray(existingConfig.roots) && existingConfig.roots.every((value) => typeof value === 'string') ? existingConfig.roots : [];
|
|
197
|
+
const defaultRoots = cliRoots.length > 0 ? await normalizeRoots(cliRoots) : existingRoots.length > 0 ? existingRoots : [await suggestDefaultRoot()];
|
|
198
|
+
const defaultPort = cliPort ?? (typeof existingConfig.port === 'number' ? existingConfig.port : 3000);
|
|
199
|
+
printSetupIntro(configPath, stateDir, Object.keys(existingConfig).length > 0);
|
|
200
|
+
console.log('Tip: roots should be parent directories like ~/projects, not individual repos.');
|
|
201
|
+
console.log('Pimote scans each root one level deep and picks folders containing .git or package.json.');
|
|
202
|
+
console.log('');
|
|
203
|
+
const rl = createInterface({ input, output });
|
|
204
|
+
try {
|
|
205
|
+
const roots = cliRoots.length > 0 ? defaultRoots : await promptForRoots(rl, defaultRoots);
|
|
206
|
+
const port = cliPort ?? (await promptForPort(rl, defaultPort));
|
|
207
|
+
return { roots, port };
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
rl.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function resolveInitConfig(options) {
|
|
214
|
+
const { interactive, existingConfig, configPath, stateDir, cliPort, cliRoots } = options;
|
|
215
|
+
if (cliRoots.length > 0 && !interactive) {
|
|
216
|
+
return {
|
|
217
|
+
roots: await normalizeRoots(cliRoots),
|
|
218
|
+
port: cliPort ?? (typeof existingConfig.port === 'number' ? existingConfig.port : 3000),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
if (!interactive) {
|
|
222
|
+
throw new Error(`No interactive terminal detected. Run \`pimote init --root /path/to/projects [--port 3000]\` or create ${configPath} manually.`);
|
|
223
|
+
}
|
|
224
|
+
return runInteractiveInit({
|
|
225
|
+
interactive,
|
|
226
|
+
existingConfig,
|
|
227
|
+
configPath,
|
|
228
|
+
stateDir,
|
|
229
|
+
cliPort,
|
|
230
|
+
cliRoots,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
export async function initializeConfig(options) {
|
|
234
|
+
const existing = await readExistingConfigObject(CONFIG_PATH);
|
|
235
|
+
if (existing.warning) {
|
|
236
|
+
console.warn(existing.warning);
|
|
237
|
+
}
|
|
238
|
+
const desired = await resolveInitConfig({
|
|
239
|
+
interactive: Boolean(input.isTTY && output.isTTY),
|
|
240
|
+
existingConfig: existing.data,
|
|
241
|
+
configPath: CONFIG_PATH,
|
|
242
|
+
stateDir: PIMOTE_STATE_DIR,
|
|
243
|
+
cliPort: options.cliPort,
|
|
244
|
+
cliRoots: options.cliRoots,
|
|
245
|
+
});
|
|
246
|
+
const config = buildConfigPayload(existing.data, desired.roots, desired.port);
|
|
247
|
+
await writeConfig(CONFIG_PATH, config);
|
|
248
|
+
console.log(`[pimote] Wrote config to ${CONFIG_PATH}`);
|
|
249
|
+
console.log(`[pimote] Pimote will scan these roots:`);
|
|
250
|
+
for (const root of desired.roots) {
|
|
251
|
+
console.log(` - ${root}`);
|
|
252
|
+
}
|
|
253
|
+
console.log(`[pimote] Pimote will listen on http://localhost:${desired.port}`);
|
|
254
|
+
console.log(`[pimote] Runtime state will be stored in ${PIMOTE_STATE_DIR}`);
|
|
255
|
+
if (options.command === 'init') {
|
|
256
|
+
console.log('[pimote] Next step: run `pimote` and open the printed URL in your browser.');
|
|
257
|
+
}
|
|
258
|
+
return { configPath: CONFIG_PATH, created: !existing.exists, port: desired.port };
|
|
259
|
+
}
|
|
260
|
+
export function parseArgs(argv) {
|
|
261
|
+
const commands = new Set(['start', 'init', 'help', 'version']);
|
|
262
|
+
let command = 'start';
|
|
263
|
+
let index = 0;
|
|
264
|
+
if (argv[0] === '--help' || argv[0] === '-h') {
|
|
265
|
+
return { command: 'help', port: undefined, roots: [] };
|
|
266
|
+
}
|
|
267
|
+
if (argv[0] === '--version' || argv[0] === '-v') {
|
|
268
|
+
return { command: 'version', port: undefined, roots: [] };
|
|
269
|
+
}
|
|
270
|
+
if (argv[0] && !argv[0].startsWith('-')) {
|
|
271
|
+
if (!commands.has(argv[0])) {
|
|
272
|
+
return { command: 'start', roots: [], error: `Unknown command: ${argv[0]}` };
|
|
273
|
+
}
|
|
274
|
+
command = argv[0];
|
|
275
|
+
index = 1;
|
|
276
|
+
}
|
|
277
|
+
let port;
|
|
278
|
+
const roots = [];
|
|
279
|
+
while (index < argv.length) {
|
|
280
|
+
const arg = argv[index];
|
|
281
|
+
if (arg === '--help' || arg === '-h') {
|
|
282
|
+
return { command: 'help', port, roots };
|
|
283
|
+
}
|
|
284
|
+
if (arg === '--version' || arg === '-v') {
|
|
285
|
+
return { command: 'version', port, roots };
|
|
286
|
+
}
|
|
287
|
+
if (arg === '--port' || arg === '-p') {
|
|
288
|
+
const value = argv[index + 1];
|
|
289
|
+
if (!value)
|
|
290
|
+
return { command, port, roots, error: 'Missing value for --port' };
|
|
291
|
+
try {
|
|
292
|
+
port = parsePort(value);
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
return { command, port, roots, error: err instanceof Error ? err.message : String(err) };
|
|
296
|
+
}
|
|
297
|
+
index += 2;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (arg === '--root' || arg === '-r') {
|
|
301
|
+
const value = argv[index + 1];
|
|
302
|
+
if (!value)
|
|
303
|
+
return { command, port, roots, error: 'Missing value for --root' };
|
|
304
|
+
roots.push(value);
|
|
305
|
+
index += 2;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
return { command, port, roots, error: `Unknown option: ${arg}` };
|
|
309
|
+
}
|
|
310
|
+
return { command, port, roots };
|
|
311
|
+
}
|
|
312
|
+
export async function ensureConfigForStart(parsed) {
|
|
313
|
+
if (await pathExists(CONFIG_PATH)) {
|
|
314
|
+
return { created: false, configPath: CONFIG_PATH };
|
|
315
|
+
}
|
|
316
|
+
console.log(`[pimote] No config found at ${CONFIG_PATH}.`);
|
|
317
|
+
console.log('');
|
|
318
|
+
const result = await initializeConfig({ command: 'start', cliPort: parsed.port, cliRoots: parsed.roots });
|
|
319
|
+
console.log('');
|
|
320
|
+
console.log('[pimote] Starting server...');
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
export async function startServer(parsed) {
|
|
324
|
+
await ensureConfigForStart(parsed);
|
|
325
|
+
await startPimote({ portOverride: parsed.port });
|
|
326
|
+
}
|
|
327
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
328
|
+
const parsed = parseArgs(argv);
|
|
329
|
+
if (parsed.error) {
|
|
330
|
+
console.error(`[pimote] ${parsed.error}`);
|
|
331
|
+
console.error('Run `pimote help` for usage.');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
if (parsed.command === 'help') {
|
|
335
|
+
printHelp();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (parsed.command === 'version') {
|
|
339
|
+
console.log(await getVersion());
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (parsed.command === 'init') {
|
|
343
|
+
await initializeConfig({ command: 'init', cliPort: parsed.port, cliRoots: parsed.roots });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await startServer(parsed);
|
|
347
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { PIMOTE_CONFIG_PATH } from './paths.js';
|
|
4
|
+
export const CONFIG_PATH = PIMOTE_CONFIG_PATH;
|
|
5
|
+
const DEFAULTS = {
|
|
6
|
+
idleTimeout: 1_800_000, // 30 minutes
|
|
7
|
+
bufferSize: 1000,
|
|
8
|
+
port: 3000,
|
|
9
|
+
};
|
|
10
|
+
export async function loadConfig() {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = await readFile(CONFIG_PATH, 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {
|
|
17
|
+
throw new Error(`Config file not found at ${CONFIG_PATH}\n\n` +
|
|
18
|
+
`Create it with at least a "roots" array, e.g.:\n\n` +
|
|
19
|
+
` {\n` +
|
|
20
|
+
` "roots": ["/path/to/your/project"]\n` +
|
|
21
|
+
` }\n\n` +
|
|
22
|
+
`Optional fields: port (default ${DEFAULTS.port}), ` +
|
|
23
|
+
`idleTimeout (default ${DEFAULTS.idleTimeout}ms), ` +
|
|
24
|
+
`bufferSize (default ${DEFAULTS.bufferSize})`, { cause: err });
|
|
25
|
+
}
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`Failed to parse ${CONFIG_PATH} as JSON`);
|
|
34
|
+
}
|
|
35
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
36
|
+
throw new Error(`Config at ${CONFIG_PATH} must be a JSON object`);
|
|
37
|
+
}
|
|
38
|
+
const obj = parsed;
|
|
39
|
+
// Validate roots
|
|
40
|
+
if (!Array.isArray(obj.roots) || obj.roots.length === 0 || !obj.roots.every((r) => typeof r === 'string')) {
|
|
41
|
+
throw new Error(`Config "roots" must be a non-empty array of strings in ${CONFIG_PATH}`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
roots: obj.roots,
|
|
45
|
+
idleTimeout: typeof obj.idleTimeout === 'number' ? obj.idleTimeout : DEFAULTS.idleTimeout,
|
|
46
|
+
bufferSize: typeof obj.bufferSize === 'number' ? obj.bufferSize : DEFAULTS.bufferSize,
|
|
47
|
+
port: typeof obj.port === 'number' ? obj.port : DEFAULTS.port,
|
|
48
|
+
defaultProvider: typeof obj.defaultProvider === 'string' ? obj.defaultProvider : undefined,
|
|
49
|
+
defaultModel: typeof obj.defaultModel === 'string' ? obj.defaultModel : undefined,
|
|
50
|
+
defaultThinkingLevel: typeof obj.defaultThinkingLevel === 'string' ? obj.defaultThinkingLevel : undefined,
|
|
51
|
+
vapidPublicKey: typeof obj.vapidPublicKey === 'string' ? obj.vapidPublicKey : undefined,
|
|
52
|
+
vapidPrivateKey: typeof obj.vapidPrivateKey === 'string' ? obj.vapidPrivateKey : undefined,
|
|
53
|
+
vapidEmail: typeof obj.vapidEmail === 'string' ? obj.vapidEmail : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export async function ensureVapidKeys(config) {
|
|
57
|
+
if (config.vapidPublicKey && config.vapidPrivateKey) {
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
60
|
+
const webpush = await import('web-push');
|
|
61
|
+
const keys = webpush.default.generateVAPIDKeys();
|
|
62
|
+
config.vapidPublicKey = keys.publicKey;
|
|
63
|
+
config.vapidPrivateKey = keys.privateKey;
|
|
64
|
+
// Read existing file to preserve all fields, then merge in new keys
|
|
65
|
+
let existing = {};
|
|
66
|
+
try {
|
|
67
|
+
const raw = await readFile(CONFIG_PATH, 'utf-8');
|
|
68
|
+
existing = JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// If file doesn't exist or can't be parsed, start fresh
|
|
72
|
+
}
|
|
73
|
+
existing.vapidPublicKey = keys.publicKey;
|
|
74
|
+
existing.vapidPrivateKey = keys.privateKey;
|
|
75
|
+
await mkdir(dirname(CONFIG_PATH), { recursive: true });
|
|
76
|
+
await writeFile(CONFIG_PATH, JSON.stringify(existing, null, 2) + '\n', 'utf-8');
|
|
77
|
+
return config;
|
|
78
|
+
}
|