@gogomi/pi-windows-shell 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/output.ts ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Output truncation and tail reading utilities
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import { createReadStream } from "node:fs";
7
+ import type { TailResult } from "./types.js";
8
+
9
+ const DEFAULT_MAX_BYTES = 50000;
10
+ const DEFAULT_TAIL_LINES = 100;
11
+
12
+ /**
13
+ * Read lines from the end of a file (tail functionality).
14
+ */
15
+ export async function tailFile(
16
+ filePath: string,
17
+ options: {
18
+ lines?: number;
19
+ maxBytes?: number;
20
+ } = {}
21
+ ): Promise<TailResult> {
22
+ const lines = options.lines ?? DEFAULT_TAIL_LINES;
23
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
24
+
25
+ try {
26
+ await fs.promises.access(filePath);
27
+ } catch {
28
+ return {
29
+ lines: [],
30
+ truncated: false,
31
+ totalLines: 0,
32
+ linesRead: 0,
33
+ fileExists: false,
34
+ };
35
+ }
36
+
37
+ try {
38
+ const stats = await fs.promises.stat(filePath);
39
+ const fileSize = stats.size;
40
+
41
+ // If file is small enough, read it all
42
+ if (fileSize <= maxBytes) {
43
+ const content = await fs.promises.readFile(filePath, "utf-8");
44
+ const allLines = content.split(/\r?\n/).filter((l) => l.length > 0);
45
+ const tailLines = allLines.slice(-lines);
46
+
47
+ return {
48
+ lines: tailLines,
49
+ truncated: allLines.length > tailLines.length,
50
+ totalLines: allLines.length,
51
+ linesRead: tailLines.length,
52
+ fileExists: true,
53
+ };
54
+ }
55
+
56
+ // For large files, stream from the end
57
+ const buffer = Buffer.alloc(maxBytes);
58
+ const fd = await fs.promises.open(filePath, "r");
59
+
60
+ try {
61
+ // Read from near the end
62
+ const startPos = Math.max(0, fileSize - maxBytes);
63
+ const { bytesRead } = await fd.read(buffer, 0, maxBytes, startPos);
64
+ const content = buffer.toString("utf-8", 0, bytesRead);
65
+
66
+ // Split and find tail lines
67
+ const allLines = content.split(/\r?\n/);
68
+
69
+ // Skip partial first line if we started mid-line
70
+ const skipPartial = startPos > 0 && !content.startsWith("\n") && !content.startsWith("\r");
71
+ const lineStart = skipPartial ? 1 : 0;
72
+
73
+ const totalLines = allLines.length - lineStart;
74
+ const tailStartIndex = Math.max(lineStart, allLines.length - lines);
75
+ const tailLines = allLines.slice(tailStartIndex);
76
+
77
+ return {
78
+ lines: tailLines,
79
+ truncated: tailStartIndex > lineStart,
80
+ totalLines,
81
+ linesRead: tailLines.length,
82
+ fileExists: true,
83
+ };
84
+ } finally {
85
+ await fd.close();
86
+ }
87
+ } catch (error) {
88
+ throw new Error(
89
+ `Failed to read output file: ${filePath}. Error: ${error instanceof Error ? error.message : String(error)}`
90
+ );
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Truncate output text to a maximum size.
96
+ */
97
+ export function truncateOutput(
98
+ text: string,
99
+ options: {
100
+ maxBytes?: number;
101
+ maxLines?: number;
102
+ } = {}
103
+ ): { text: string; truncated: boolean } {
104
+ const maxBytes = options.maxBytes ?? 50000;
105
+ const maxLines = options.maxLines ?? 2000;
106
+
107
+ // Check line limit
108
+ const lines = text.split(/\r?\n/);
109
+ if (lines.length > maxLines) {
110
+ const truncatedLines = lines.slice(0, maxLines);
111
+ return {
112
+ text: truncatedLines.join("\n") + `\n[output truncated: showing first ${maxLines} lines of ${lines.length}]`,
113
+ truncated: true,
114
+ };
115
+ }
116
+
117
+ // Check byte limit
118
+ if (Buffer.byteLength(text, "utf-8") > maxBytes) {
119
+ let truncatedText = "";
120
+ let currentBytes = 0;
121
+
122
+ for (const line of lines) {
123
+ const lineBytes = Buffer.byteLength(line + "\n", "utf-8");
124
+ if (currentBytes + lineBytes > maxBytes) {
125
+ break;
126
+ }
127
+ truncatedText += line + "\n";
128
+ currentBytes += lineBytes;
129
+ }
130
+
131
+ return {
132
+ text: truncatedText.trimEnd() + `\n[output truncated: ${Buffer.byteLength(text, "utf-8")} bytes down to ${maxBytes} bytes]`,
133
+ truncated: true,
134
+ };
135
+ }
136
+
137
+ return { text, truncated: false };
138
+ }
139
+
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@gogomi/pi-windows-shell",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Windows PowerShell and process-management tools for Pi coding agent.",
6
+ "main": "index.ts",
7
+ "files": [
8
+ "index.ts",
9
+ "shell.ts",
10
+ "output.ts",
11
+ "paths.ts",
12
+ "process-registry.ts",
13
+ "types.ts",
14
+ "prompts/windows-shell-policy.md",
15
+ "tsconfig.json",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "keywords": [
20
+ "pi-package",
21
+ "pi-extension",
22
+ "powershell",
23
+ "windows",
24
+ "process-management"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/Gogomy/pi-windows-shell.git"
29
+ },
30
+ "license": "MIT",
31
+ "pi": {
32
+ "extensions": [
33
+ "./index.ts"
34
+ ],
35
+ "prompts": [
36
+ "./prompts"
37
+ ]
38
+ },
39
+ "dependencies": {
40
+ "@sinclair/typebox": "^0.34.0"
41
+ },
42
+ "peerDependencies": {
43
+ "@earendil-works/pi-coding-agent": "*"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.0.0"
47
+ }
48
+ }
package/paths.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Path helpers for pi-windows-shell
3
+ */
4
+
5
+ import path from "node:path";
6
+ import os from "node:os";
7
+
8
+ /**
9
+ * Get the base directory for pi-windows-shell data.
10
+ * Uses LOCALAPPDATA, falls back to TEMP.
11
+ */
12
+ export function getBaseDir(): string {
13
+ const localAppData = process.env.LOCALAPPDATA;
14
+ if (localAppData) {
15
+ return path.join(localAppData, "pi-windows-shell");
16
+ }
17
+ return path.join(os.tmpdir(), "pi-windows-shell");
18
+ }
19
+
20
+ /**
21
+ * Get the registry file path.
22
+ */
23
+ export function getRegistryPath(): string {
24
+ return path.join(getBaseDir(), "processes.json");
25
+ }
26
+
27
+ /**
28
+ * Get the logs directory path.
29
+ */
30
+ export function getLogsDir(): string {
31
+ return path.join(getBaseDir(), "logs");
32
+ }
33
+
34
+ /**
35
+ * Generate a default output file path for a new process.
36
+ */
37
+ export function getDefaultOutputFile(name: string): string {
38
+ const timestamp = Date.now().toString(36);
39
+ const safeName = (name || "process")
40
+ .replace(/[^a-zA-Z0-9_-]/g, "_")
41
+ .replace(/_{2,}/g, "_")
42
+ .slice(0, 50);
43
+
44
+ const logsDir = getLogsDir();
45
+ return path.join(logsDir, `${timestamp}-${safeName}.log`);
46
+ }
47
+
48
+ /**
49
+ * Ensure a directory exists, creating it if necessary.
50
+ */
51
+ export async function ensureDir(dirPath: string): Promise<void> {
52
+ const { mkdir } = await import("node:fs/promises");
53
+ await mkdir(dirPath, { recursive: true });
54
+ }
55
+
56
+ /**
57
+ * Generate a unique process ID.
58
+ */
59
+ export function generateProcessId(name: string): string {
60
+ const timestamp = Date.now().toString(36);
61
+ const safeName = (name || "process")
62
+ .replace(/[^a-zA-Z0-9_-]/g, "_")
63
+ .slice(0, 20);
64
+ return `${timestamp}-${safeName}`;
65
+ }
66
+
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Persistent process registry for pi-windows-shell
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import type { ProcessRegistry, ManagedProcess } from "./types.js";
7
+ import { getRegistryPath, ensureDir, getBaseDir } from "./paths.js";
8
+
9
+ const REGISTRY_VERSION = "1.0.0";
10
+
11
+ /**
12
+ * Ensure the base directory exists.
13
+ */
14
+ async function ensureBaseDir(): Promise<void> {
15
+ await ensureDir(getBaseDir());
16
+ }
17
+
18
+ /**
19
+ * Load the process registry from disk.
20
+ */
21
+ export async function loadRegistry(): Promise<ProcessRegistry> {
22
+ try {
23
+ await ensureBaseDir();
24
+ const path = getRegistryPath();
25
+
26
+ try {
27
+ await fs.promises.access(path);
28
+ } catch {
29
+ // File doesn't exist, return empty registry
30
+ return { version: REGISTRY_VERSION, processes: [] };
31
+ }
32
+
33
+ const content = await fs.promises.readFile(path, "utf-8");
34
+ const data = JSON.parse(content) as ProcessRegistry;
35
+
36
+ // Validate version
37
+ if (!data.version || !Array.isArray(data.processes)) {
38
+ return { version: REGISTRY_VERSION, processes: [] };
39
+ }
40
+
41
+ return data;
42
+ } catch (error) {
43
+ console.error(`Failed to load registry: ${error}`);
44
+ return { version: REGISTRY_VERSION, processes: [] };
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Save the process registry to disk.
50
+ */
51
+ export async function saveRegistry(registry: ProcessRegistry): Promise<void> {
52
+ try {
53
+ await ensureBaseDir();
54
+ const path = getRegistryPath();
55
+ await fs.promises.writeFile(path, JSON.stringify(registry, null, 2), "utf-8");
56
+ } catch (error) {
57
+ throw new Error(
58
+ `Failed to save registry: ${error instanceof Error ? error.message : String(error)}`
59
+ );
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Add a new process to the registry.
65
+ */
66
+ export async function addProcess(process: ManagedProcess): Promise<void> {
67
+ const registry = await loadRegistry();
68
+ registry.processes.push(process);
69
+ await saveRegistry(registry);
70
+ }
71
+
72
+ /**
73
+ * Update a process in the registry.
74
+ */
75
+ export async function updateProcess(id: string, updates: Partial<ManagedProcess>): Promise<boolean> {
76
+ const registry = await loadRegistry();
77
+ const index = registry.processes.findIndex((p) => p.id === id);
78
+
79
+ if (index === -1) {
80
+ return false;
81
+ }
82
+
83
+ registry.processes[index] = { ...registry.processes[index], ...updates };
84
+ await saveRegistry(registry);
85
+ return true;
86
+ }
87
+
88
+ /**
89
+ * Remove a process from the registry.
90
+ */
91
+ export async function removeProcess(id: string): Promise<boolean> {
92
+ const registry = await loadRegistry();
93
+ const initialLength = registry.processes.length;
94
+ registry.processes = registry.processes.filter((p) => p.id !== id);
95
+
96
+ if (registry.processes.length === initialLength) {
97
+ return false;
98
+ }
99
+
100
+ await saveRegistry(registry);
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * Get a process by ID.
106
+ */
107
+ export async function getProcess(id: string): Promise<ManagedProcess | null> {
108
+ const registry = await loadRegistry();
109
+ return registry.processes.find((p) => p.id === id) ?? null;
110
+ }
111
+
112
+ /**
113
+ * Get all processes.
114
+ */
115
+ export async function getAllProcesses(): Promise<ManagedProcess[]> {
116
+ const registry = await loadRegistry();
117
+ return registry.processes;
118
+ }
119
+
120
+ /**
121
+ * Remove multiple processes by IDs.
122
+ */
123
+ export async function removeProcesses(ids: string[]): Promise<number> {
124
+ const registry = await loadRegistry();
125
+ const initialLength = registry.processes.length;
126
+ const idSet = new Set(ids);
127
+ registry.processes = registry.processes.filter((p) => !idSet.has(p.id));
128
+
129
+ const removed = initialLength - registry.processes.length;
130
+ if (removed > 0) {
131
+ await saveRegistry(registry);
132
+ }
133
+ return removed;
134
+ }
135
+
136
+ /**
137
+ * Clean up stale registry entries.
138
+ */
139
+ export async function cleanupRegistry(options: {
140
+ removeExited?: boolean;
141
+ deleteLogs?: boolean;
142
+ olderThanDays?: number;
143
+ }): Promise<{
144
+ removedEntries: number;
145
+ deletedLogs: number;
146
+ keptRunning: number;
147
+ }> {
148
+ const registry = await loadRegistry();
149
+ const toRemove: string[] = [];
150
+ let deletedLogs = 0;
151
+
152
+ // Handle log deletion
153
+ if (options.deleteLogs && options.olderThanDays) {
154
+ const olderThan = Date.now() - options.olderThanDays * 24 * 60 * 60 * 1000;
155
+
156
+ for (const proc of registry.processes) {
157
+ try {
158
+ const stat = await fs.promises.stat(proc.outputFile);
159
+ if (stat.mtimeMs < olderThan) {
160
+ await fs.promises.unlink(proc.outputFile);
161
+ deletedLogs++;
162
+ }
163
+ } catch {
164
+ // File doesn't exist or can't be accessed, skip
165
+ }
166
+ }
167
+ }
168
+
169
+ // Handle registry entry removal
170
+ if (options.removeExited) {
171
+ for (const proc of registry.processes) {
172
+ if (proc.status === "exited") {
173
+ toRemove.push(proc.id);
174
+ }
175
+ }
176
+ }
177
+
178
+ const removedEntries = toRemove.length;
179
+ if (removedEntries > 0) {
180
+ await removeProcesses(toRemove);
181
+ }
182
+
183
+ // Count kept running processes, excluding those just removed
184
+ const removedIds = new Set(toRemove);
185
+ const remaining = registry.processes.filter(
186
+ (p) => !removedIds.has(p.id) && p.status === "running"
187
+ );
188
+
189
+ return {
190
+ removedEntries,
191
+ deletedLogs,
192
+ keptRunning: remaining.length,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Check if a process with given PID is still alive.
198
+ * Returns the process status or null if not found.
199
+ */
200
+ export async function checkProcessAlive(pid: number): Promise<"running" | "exited" | "unknown"> {
201
+ try {
202
+ // Try using Node's process kill check
203
+ process.kill(pid, 0);
204
+ return "running";
205
+ } catch {
206
+ // ESRCH means process doesn't exist
207
+ // Other errors may mean permission issues
208
+ return "exited";
209
+ }
210
+ }
@@ -0,0 +1,113 @@
1
+ # Windows Shell Policy
2
+
3
+ The environment runs on Windows.
4
+
5
+ These rules are always active when choosing how to run commands.
6
+
7
+ ## Shell/tool selection
8
+
9
+ Use Bash only for Git-oriented and Unix-like repository inspection workflows:
10
+
11
+ - `git status`
12
+ - `git diff`
13
+ - `git log`
14
+ - `git grep`
15
+ - patch inspection
16
+ - Unix-style text pipelines
17
+ - `grep`, `sed`, `awk`, `find`, `xargs`
18
+
19
+ Use PowerShell for Windows-native workflows:
20
+
21
+ - Windows paths such as `C:\...` or `D:\...`
22
+ - Windows environment variables such as `$env:USERPROFILE`
23
+ - `.exe`, `.cmd`, `.bat`, `.ps1`
24
+ - process management
25
+ - killing processes by PID or port
26
+ - checking installed commands with `Get-Command`
27
+ - Python virtual environments on Windows
28
+ - npm/yarn/pnpm/npx commands that resolve through `.cmd` launchers
29
+ - Windows services, registry, or system configuration
30
+ - launching Windows executables such as Godot, Blender, editors, or installers
31
+
32
+ Do not mix Bash syntax and PowerShell syntax in the same command.
33
+
34
+ Prefer `git diff` over shell-specific `diff` when inspecting repository changes.
35
+
36
+ ## Windows path handling
37
+
38
+ Do not assume Windows paths are valid inside Bash.
39
+
40
+ If a command uses a Windows path such as:
41
+
42
+ ```txt
43
+ C:\...
44
+ D:\...
45
+ ```
46
+
47
+ use PowerShell.
48
+
49
+ Do not manually convert Windows paths to `/mnt/c/...`, `/c/...`, or `/d/...` unless the active Bash environment has already been verified to support that path style.
50
+
51
+ Do not assume WSL is available.
52
+
53
+ ## Long-running commands
54
+
55
+ Do not use Bash background syntax on Windows:
56
+
57
+ ```bash
58
+ npm run dev &
59
+ ```
60
+
61
+ For long-running commands, background servers, file watchers, REPLs, or GUI applications, use the environment-provided Windows process tools when available:
62
+
63
+ - `win_start_process`
64
+ - `win_process_status`
65
+ - `win_read_output`
66
+ - `win_stop_process`
67
+ - `win_list_processes`
68
+
69
+ For stuck ports, use the environment-provided port tool when available:
70
+
71
+ - `win_kill_port`
72
+
73
+ Examples of long-running or blocking commands include:
74
+
75
+ - `npm run dev`
76
+ - `pnpm dev`
77
+ - `yarn dev`
78
+ - `godot . -e`
79
+ - local web servers
80
+ - file watchers
81
+ - REPLs
82
+ - GUI applications
83
+
84
+ ## Command availability
85
+
86
+ Do not assume project-specific executable paths or tool names globally.
87
+
88
+ If a command is unavailable, diagnose it before retrying.
89
+
90
+ For PowerShell-native checks, use:
91
+
92
+ ```powershell
93
+ Get-Command <command-name> -ErrorAction SilentlyContinue
94
+ ```
95
+
96
+ If the environment provides `win_which`, it may also be used for Windows command resolution.
97
+
98
+ For Bash-only tools, use Bash diagnostics only after confirming the command is intended to run in Bash.
99
+
100
+ ## Failure handling
101
+
102
+ When a command fails, do not retry blindly.
103
+
104
+ Identify:
105
+
106
+ 1. Which shell/tool ran it.
107
+ 2. The current working directory.
108
+ 3. The exit code.
109
+ 4. stderr/stdout.
110
+ 5. Whether the command is Windows-native, Unix-like, or environment-provided.
111
+ 6. Whether the path syntax matches the selected shell.
112
+
113
+ Then choose the corrected shell or tool before retrying.