@pi-unipi/core 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/README.md +74 -0
- package/constants.ts +93 -0
- package/events.ts +106 -0
- package/index.ts +10 -0
- package/package.json +32 -0
- package/sandbox.ts +85 -0
- package/utils.ts +196 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @unipi/core
|
|
2
|
+
|
|
3
|
+
Shared utilities, event types, and constants for the [Unipi](https://github.com/Neuron-Mr-White/unipi) extension suite.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:@unipi/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or as part of the full suite:
|
|
12
|
+
```bash
|
|
13
|
+
pi install npm:unipi
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { UNIPI_EVENTS, MODULES, sanitize, emitEvent } from "@unipi/core";
|
|
20
|
+
|
|
21
|
+
// Emit module ready event
|
|
22
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
23
|
+
name: MODULES.WORKFLOW,
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
commands: ["brainstorm", "plan"],
|
|
26
|
+
tools: [],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Use shared utilities
|
|
30
|
+
const safeName = sanitize("my/feature: branch");
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Exports
|
|
34
|
+
|
|
35
|
+
### Constants
|
|
36
|
+
- `UNIPI_PREFIX` — Command prefix (`unipi:`)
|
|
37
|
+
- `MODULES` — All module names
|
|
38
|
+
- `WORKFLOW_COMMANDS` — Workflow command names
|
|
39
|
+
- `RALPH_COMMANDS` — Ralph command names
|
|
40
|
+
- `RALPH_TOOLS` — Ralph tool names
|
|
41
|
+
- `RALPH_DEFAULTS` — Default ralph settings
|
|
42
|
+
- `RALPH_DIR` — Ralph state directory
|
|
43
|
+
- `RALPH_COMPLETE_MARKER` — Loop completion marker
|
|
44
|
+
|
|
45
|
+
### Events
|
|
46
|
+
- `UNIPI_EVENTS` — Event names
|
|
47
|
+
- `UnipiModuleEvent` — Module ready/gone payload
|
|
48
|
+
- `UnipiWorkflowEvent` — Workflow start/end payload
|
|
49
|
+
- `UnipiRalphLoopEvent` — Ralph loop start/end payload
|
|
50
|
+
- `UnipiRalphIterationEvent` — Ralph iteration payload
|
|
51
|
+
- `UnipiStatusRequestEvent` / `UnipiStatusResponseEvent` — Status payloads
|
|
52
|
+
|
|
53
|
+
### Utilities
|
|
54
|
+
- `sanitize(name)` — Sanitize string for filenames
|
|
55
|
+
- `ensureDir(path)` — Create parent directories
|
|
56
|
+
- `tryDelete(path)` — Safe file deletion
|
|
57
|
+
- `tryRead(path)` — Safe file read
|
|
58
|
+
- `safeMtimeMs(path)` — File modification time
|
|
59
|
+
- `tryRemoveDir(path)` — Safe directory removal
|
|
60
|
+
- `resolvePath(cwd, path)` — Resolve relative/absolute paths
|
|
61
|
+
- `fileExists(path)` — Check file existence
|
|
62
|
+
- `writeFile(path, content)` — Write file with dir creation
|
|
63
|
+
- `readJson<T>(path)` — Read JSON file
|
|
64
|
+
- `writeJson(path, data)` — Write JSON file
|
|
65
|
+
- `randomId(length)` — Generate random ID
|
|
66
|
+
- `now()` — ISO timestamp
|
|
67
|
+
- `parseArgs(str)` — Parse quoted arguments
|
|
68
|
+
- `getPackageVersion(dir)` — Read package version
|
|
69
|
+
- `isModuleAvailable(cwd, name)` — Check if npm module exists
|
|
70
|
+
- `emitEvent(pi, name, payload)` — Safe event emission
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT
|
package/constants.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/core — Shared constants
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Prefix for all unipi commands */
|
|
6
|
+
export const UNIPI_PREFIX = "unipi:" as const;
|
|
7
|
+
|
|
8
|
+
/** Ralph loop state directory */
|
|
9
|
+
export const RALPH_DIR = ".unipi/ralph" as const;
|
|
10
|
+
|
|
11
|
+
/** Ralph completion marker */
|
|
12
|
+
export const RALPH_COMPLETE_MARKER = "<promise>COMPLETE</promise>" as const;
|
|
13
|
+
|
|
14
|
+
/** Unipi settings key in pi settings.json */
|
|
15
|
+
export const UNIPI_SETTINGS_KEY = "unipi" as const;
|
|
16
|
+
|
|
17
|
+
/** Module names */
|
|
18
|
+
export const MODULES = {
|
|
19
|
+
CORE: "@unipi/core",
|
|
20
|
+
WORKFLOW: "@unipi/workflow",
|
|
21
|
+
RALPH: "@unipi/ralph",
|
|
22
|
+
SUBAGENTS: "@unipi/subagents",
|
|
23
|
+
MEMORY: "@unipi/memory",
|
|
24
|
+
REGISTRY: "@unipi/registry",
|
|
25
|
+
MCP: "@unipi/mcp",
|
|
26
|
+
TASK: "@unipi/task",
|
|
27
|
+
WEBTOOLS: "@unipi/webtools",
|
|
28
|
+
INFO_SCREEN: "@unipi/info-screen",
|
|
29
|
+
IMPECCABLE: "@unipi/impeccable",
|
|
30
|
+
SETTINGS: "@unipi/settings",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
/** Workflow command names */
|
|
34
|
+
export const WORKFLOW_COMMANDS = {
|
|
35
|
+
BRAINSTORM: "brainstorm",
|
|
36
|
+
PLAN: "plan",
|
|
37
|
+
WORK: "work",
|
|
38
|
+
REVIEW_WORK: "review-work",
|
|
39
|
+
CONSOLIDATE: "consolidate",
|
|
40
|
+
WORKTREE_CREATE: "worktree-create",
|
|
41
|
+
WORKTREE_LIST: "worktree-list",
|
|
42
|
+
WORKTREE_MERGE: "worktree-merge",
|
|
43
|
+
CONSULTANT: "consultant",
|
|
44
|
+
QUICK_WORK: "quick-work",
|
|
45
|
+
GATHER_CONTEXT: "gather-context",
|
|
46
|
+
DOCUMENT: "document",
|
|
47
|
+
SCAN_ISSUES: "scan-issues",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/** Ralph command names */
|
|
51
|
+
export const RALPH_COMMANDS = {
|
|
52
|
+
START: "ralph-start",
|
|
53
|
+
STOP: "ralph-stop",
|
|
54
|
+
RESUME: "ralph-resume",
|
|
55
|
+
STATUS: "ralph-status",
|
|
56
|
+
CANCEL: "ralph-cancel",
|
|
57
|
+
ARCHIVE: "ralph-archive",
|
|
58
|
+
CLEAN: "ralph-clean",
|
|
59
|
+
LIST: "ralph-list",
|
|
60
|
+
NUKE: "ralph-nuke",
|
|
61
|
+
} as const;
|
|
62
|
+
|
|
63
|
+
/** Ralph tool names */
|
|
64
|
+
export const RALPH_TOOLS = {
|
|
65
|
+
START: "ralph_start",
|
|
66
|
+
DONE: "ralph_done",
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/** Unipi directory paths */
|
|
70
|
+
export const UNIPI_DIRS = {
|
|
71
|
+
ROOT: ".unipi",
|
|
72
|
+
DOCS: ".unipi/docs",
|
|
73
|
+
SPECS: ".unipi/docs/specs",
|
|
74
|
+
PLANS: ".unipi/docs/plans",
|
|
75
|
+
GENERATED: ".unipi/docs/generated",
|
|
76
|
+
REVIEWS: ".unipi/docs/reviews",
|
|
77
|
+
MEMORY: ".unipi/memory",
|
|
78
|
+
QUICK_WORK: ".unipi/quick-work",
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
/** Default ralph loop settings */
|
|
82
|
+
export const RALPH_DEFAULTS = {
|
|
83
|
+
MAX_ITERATIONS: 50,
|
|
84
|
+
ITEMS_PER_ITERATION: 0,
|
|
85
|
+
REFLECT_EVERY: 0,
|
|
86
|
+
} as const;
|
|
87
|
+
|
|
88
|
+
/** Status icons for ralph loops */
|
|
89
|
+
export const RALPH_STATUS_ICONS = {
|
|
90
|
+
active: "▶",
|
|
91
|
+
paused: "⏸",
|
|
92
|
+
completed: "✓",
|
|
93
|
+
} as const;
|
package/events.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/core — Event type definitions for inter-module communication
|
|
3
|
+
*
|
|
4
|
+
* Modules announce presence via pi.events. Other modules listen and
|
|
5
|
+
* enable integration features when peers are detected.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Event names emitted by unipi modules */
|
|
9
|
+
export const UNIPI_EVENTS = {
|
|
10
|
+
/** Module loaded and ready */
|
|
11
|
+
MODULE_READY: "unipi:module:ready",
|
|
12
|
+
/** Module unloading */
|
|
13
|
+
MODULE_GONE: "unipi:module:gone",
|
|
14
|
+
|
|
15
|
+
/** Workflow command started */
|
|
16
|
+
WORKFLOW_START: "unipi:workflow:start",
|
|
17
|
+
/** Workflow command ended */
|
|
18
|
+
WORKFLOW_END: "unipi:workflow:end",
|
|
19
|
+
|
|
20
|
+
/** Ralph loop started */
|
|
21
|
+
RALPH_LOOP_START: "unipi:ralph:loop:start",
|
|
22
|
+
/** Ralph loop ended */
|
|
23
|
+
RALPH_LOOP_END: "unipi:ralph:loop:end",
|
|
24
|
+
/** Ralph loop iteration completed */
|
|
25
|
+
RALPH_ITERATION_DONE: "unipi:ralph:iteration:done",
|
|
26
|
+
|
|
27
|
+
/** Request module status (for info-screen) */
|
|
28
|
+
MODULE_STATUS_REQUEST: "unipi:module:status:request",
|
|
29
|
+
/** Module status response */
|
|
30
|
+
MODULE_STATUS_RESPONSE: "unipi:module:status:response",
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
/** Payload for MODULE_READY / MODULE_GONE */
|
|
34
|
+
export interface UnipiModuleEvent {
|
|
35
|
+
/** Module name, e.g. "@unipi/workflow" */
|
|
36
|
+
name: string;
|
|
37
|
+
/** Module version */
|
|
38
|
+
version: string;
|
|
39
|
+
/** Commands registered by this module */
|
|
40
|
+
commands: string[];
|
|
41
|
+
/** Tools registered by this module */
|
|
42
|
+
tools: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Payload for WORKFLOW_START / WORKFLOW_END */
|
|
46
|
+
export interface UnipiWorkflowEvent {
|
|
47
|
+
/** Command name, e.g. "brainstorm" */
|
|
48
|
+
command: string;
|
|
49
|
+
/** Full command with prefix, e.g. "/unipi:brainstorm" */
|
|
50
|
+
fullCommand: string;
|
|
51
|
+
/** Arguments passed to command */
|
|
52
|
+
args: string;
|
|
53
|
+
/** For WORKFLOW_END: whether it succeeded */
|
|
54
|
+
success?: boolean;
|
|
55
|
+
/** For WORKFLOW_END: duration in ms */
|
|
56
|
+
durationMs?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Payload for RALPH_LOOP_START / RALPH_LOOP_END */
|
|
60
|
+
export interface UnipiRalphLoopEvent {
|
|
61
|
+
/** Loop name */
|
|
62
|
+
name: string;
|
|
63
|
+
/** Current iteration */
|
|
64
|
+
iteration: number;
|
|
65
|
+
/** Max iterations (0 = unlimited) */
|
|
66
|
+
maxIterations: number;
|
|
67
|
+
/** Loop status */
|
|
68
|
+
status: "active" | "paused" | "completed";
|
|
69
|
+
/** For RALPH_LOOP_END: reason */
|
|
70
|
+
reason?: "completed" | "max_reached" | "cancelled" | "error";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Payload for RALPH_ITERATION_DONE */
|
|
74
|
+
export interface UnipiRalphIterationEvent {
|
|
75
|
+
/** Loop name */
|
|
76
|
+
name: string;
|
|
77
|
+
/** Iteration that just completed */
|
|
78
|
+
iteration: number;
|
|
79
|
+
/** Next iteration number */
|
|
80
|
+
nextIteration: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Payload for MODULE_STATUS_REQUEST */
|
|
84
|
+
export interface UnipiStatusRequestEvent {
|
|
85
|
+
/** Request ID for correlation */
|
|
86
|
+
requestId: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Payload for MODULE_STATUS_RESPONSE */
|
|
90
|
+
export interface UnipiStatusResponseEvent {
|
|
91
|
+
/** Request ID this responds to */
|
|
92
|
+
requestId: string;
|
|
93
|
+
/** Module name */
|
|
94
|
+
name: string;
|
|
95
|
+
/** Module status data */
|
|
96
|
+
status: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Union of all unipi event payloads */
|
|
100
|
+
export type UnipiEventPayload =
|
|
101
|
+
| UnipiModuleEvent
|
|
102
|
+
| UnipiWorkflowEvent
|
|
103
|
+
| UnipiRalphLoopEvent
|
|
104
|
+
| UnipiRalphIterationEvent
|
|
105
|
+
| UnipiStatusRequestEvent
|
|
106
|
+
| UnipiStatusResponseEvent;
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pi-unipi/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared utilities, event types, and constants for Unipi extension suite",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Neuron Mr White",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Neuron-Mr-White/unipi.git",
|
|
11
|
+
"directory": "packages/core"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"pi-package",
|
|
15
|
+
"pi-extension",
|
|
16
|
+
"unipi"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"*.ts",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
27
|
+
"@sinclair/typebox": "*"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/sandbox.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/core — Sandbox module
|
|
3
|
+
*
|
|
4
|
+
* Defines tool access levels for workflow commands.
|
|
5
|
+
* Used with pi.setActiveTools() to enforce restrictions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { WORKFLOW_COMMANDS } from "./constants.js";
|
|
9
|
+
|
|
10
|
+
/** Sandbox levels */
|
|
11
|
+
export type SandboxLevel = "read_only" | "brainstorm" | "write_unipi" | "full";
|
|
12
|
+
|
|
13
|
+
/** Tool sets per sandbox level */
|
|
14
|
+
const SANDBOX_TOOLS: Record<SandboxLevel, readonly string[]> = {
|
|
15
|
+
/** Only read-only tools — no bash, no write, no edit */
|
|
16
|
+
read_only: ["read", "grep", "find", "ls"],
|
|
17
|
+
/** Read + constrained write — only to .unipi/docs/specs/ */
|
|
18
|
+
brainstorm: ["read", "grep", "find", "ls", "write"],
|
|
19
|
+
/** Read + write/edit — bash blocked, writes go through write tool */
|
|
20
|
+
write_unipi: ["read", "write", "edit"],
|
|
21
|
+
/** All tools */
|
|
22
|
+
full: ["read", "write", "edit", "bash"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Command to sandbox level mapping */
|
|
26
|
+
const COMMAND_SANDBOX: Record<string, SandboxLevel> = {
|
|
27
|
+
[WORKFLOW_COMMANDS.BRAINSTORM]: "brainstorm",
|
|
28
|
+
[WORKFLOW_COMMANDS.PLAN]: "write_unipi",
|
|
29
|
+
[WORKFLOW_COMMANDS.WORK]: "full",
|
|
30
|
+
[WORKFLOW_COMMANDS.REVIEW_WORK]: "read_only",
|
|
31
|
+
[WORKFLOW_COMMANDS.CONSOLIDATE]: "write_unipi",
|
|
32
|
+
[WORKFLOW_COMMANDS.WORKTREE_CREATE]: "full",
|
|
33
|
+
[WORKFLOW_COMMANDS.WORKTREE_LIST]: "read_only",
|
|
34
|
+
[WORKFLOW_COMMANDS.WORKTREE_MERGE]: "full",
|
|
35
|
+
[WORKFLOW_COMMANDS.CONSULTANT]: "read_only",
|
|
36
|
+
[WORKFLOW_COMMANDS.QUICK_WORK]: "full",
|
|
37
|
+
[WORKFLOW_COMMANDS.GATHER_CONTEXT]: "read_only",
|
|
38
|
+
[WORKFLOW_COMMANDS.DOCUMENT]: "write_unipi",
|
|
39
|
+
[WORKFLOW_COMMANDS.SCAN_ISSUES]: "read_only",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get sandbox level for a command.
|
|
44
|
+
*/
|
|
45
|
+
export function getSandboxLevel(commandName: string): SandboxLevel {
|
|
46
|
+
return COMMAND_SANDBOX[commandName] ?? "full";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get allowed tools for a sandbox level.
|
|
51
|
+
*/
|
|
52
|
+
export function getToolsForLevel(level: SandboxLevel): readonly string[] {
|
|
53
|
+
return SANDBOX_TOOLS[level];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get allowed tools for a command.
|
|
58
|
+
*/
|
|
59
|
+
export function getToolsForCommand(commandName: string): readonly string[] {
|
|
60
|
+
const level = getSandboxLevel(commandName);
|
|
61
|
+
return getToolsForLevel(level);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a tool is allowed at a sandbox level.
|
|
66
|
+
*/
|
|
67
|
+
export function isToolAllowed(level: SandboxLevel, toolName: string): boolean {
|
|
68
|
+
return SANDBOX_TOOLS[level].includes(toolName);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a command has write access.
|
|
73
|
+
*/
|
|
74
|
+
export function hasWriteAccess(commandName: string): boolean {
|
|
75
|
+
const level = getSandboxLevel(commandName);
|
|
76
|
+
return level === "write_unipi" || level === "full";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if a command has bash access.
|
|
81
|
+
*/
|
|
82
|
+
export function hasBashAccess(commandName: string): boolean {
|
|
83
|
+
const level = getSandboxLevel(commandName);
|
|
84
|
+
return level === "full";
|
|
85
|
+
}
|
package/utils.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @unipi/core — Shared utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanitize a string for use as a filename.
|
|
10
|
+
* Replaces non-alphanumeric chars with underscores, collapses repeats.
|
|
11
|
+
*/
|
|
12
|
+
export function sanitize(name: string): string {
|
|
13
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ensure parent directory exists for a file path.
|
|
18
|
+
*/
|
|
19
|
+
export function ensureDir(filePath: string): void {
|
|
20
|
+
const dir = path.dirname(filePath);
|
|
21
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Try to delete a file. Ignores errors.
|
|
26
|
+
*/
|
|
27
|
+
export function tryDelete(filePath: string): void {
|
|
28
|
+
try {
|
|
29
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
30
|
+
} catch {
|
|
31
|
+
/* ignore */
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Try to read a file. Returns null on error.
|
|
37
|
+
*/
|
|
38
|
+
export function tryRead(filePath: string): string | null {
|
|
39
|
+
try {
|
|
40
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get file mtime in ms. Returns 0 if file doesn't exist.
|
|
48
|
+
*/
|
|
49
|
+
export function safeMtimeMs(filePath: string): number {
|
|
50
|
+
try {
|
|
51
|
+
return fs.statSync(filePath).mtimeMs;
|
|
52
|
+
} catch {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Try to remove a directory recursively. Returns true on success.
|
|
59
|
+
*/
|
|
60
|
+
export function tryRemoveDir(dirPath: string): boolean {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(dirPath)) {
|
|
63
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a path relative to cwd, handling absolute paths.
|
|
73
|
+
*/
|
|
74
|
+
export function resolvePath(cwd: string, filePath: string): string {
|
|
75
|
+
return path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a file exists.
|
|
80
|
+
*/
|
|
81
|
+
export function fileExists(filePath: string): boolean {
|
|
82
|
+
return fs.existsSync(filePath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Write a file, ensuring parent directory exists.
|
|
87
|
+
*/
|
|
88
|
+
export function writeFile(filePath: string, content: string): void {
|
|
89
|
+
ensureDir(filePath);
|
|
90
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Read JSON file, return null on error.
|
|
95
|
+
*/
|
|
96
|
+
export function readJson<T>(filePath: string): T | null {
|
|
97
|
+
const content = tryRead(filePath);
|
|
98
|
+
if (!content) return null;
|
|
99
|
+
try {
|
|
100
|
+
return JSON.parse(content) as T;
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Write JSON file with pretty printing.
|
|
108
|
+
*/
|
|
109
|
+
export function writeJson(filePath: string, data: unknown): void {
|
|
110
|
+
writeFile(filePath, JSON.stringify(data, null, 2));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Generate a short random ID.
|
|
115
|
+
*/
|
|
116
|
+
export function randomId(length = 8): string {
|
|
117
|
+
return Math.random().toString(36).substring(2, 2 + length);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Format timestamp to ISO string.
|
|
122
|
+
*/
|
|
123
|
+
export function now(): string {
|
|
124
|
+
return new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse command arguments string into tokens.
|
|
129
|
+
* Handles quoted strings.
|
|
130
|
+
*/
|
|
131
|
+
export function parseArgs(argsStr: string): string[] {
|
|
132
|
+
return argsStr.match(/(?:[^\s"]+|"[^"]*")+/g)?.map((t) => t.replace(/^"|"$/g, "")) ?? [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get package version from package.json.
|
|
137
|
+
*/
|
|
138
|
+
export function getPackageVersion(packageDir: string): string {
|
|
139
|
+
const pkgPath = path.join(packageDir, "package.json");
|
|
140
|
+
const pkg = readJson<{ version?: string }>(pkgPath);
|
|
141
|
+
return pkg?.version ?? "0.0.0";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if a module is available in node_modules.
|
|
146
|
+
*/
|
|
147
|
+
export function isModuleAvailable(cwd: string, moduleName: string): boolean {
|
|
148
|
+
try {
|
|
149
|
+
const resolved = path.join(cwd, "node_modules", moduleName);
|
|
150
|
+
return fs.existsSync(resolved);
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Initialize .unipi directory structure.
|
|
158
|
+
* Creates all standard directories if they don't exist.
|
|
159
|
+
* Call on session_start in each extension.
|
|
160
|
+
*/
|
|
161
|
+
export function initUnipiDirs(cwd: string = process.cwd()): void {
|
|
162
|
+
const dirs = [
|
|
163
|
+
".unipi",
|
|
164
|
+
".unipi/docs",
|
|
165
|
+
".unipi/docs/specs",
|
|
166
|
+
".unipi/docs/plans",
|
|
167
|
+
".unipi/docs/generated",
|
|
168
|
+
".unipi/docs/reviews",
|
|
169
|
+
".unipi/memory",
|
|
170
|
+
".unipi/quick-work",
|
|
171
|
+
".unipi/worktrees",
|
|
172
|
+
];
|
|
173
|
+
for (const dir of dirs) {
|
|
174
|
+
const full = path.join(cwd, dir);
|
|
175
|
+
if (!fs.existsSync(full)) {
|
|
176
|
+
fs.mkdirSync(full, { recursive: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Emit a unipi event via pi.events (safe wrapper).
|
|
183
|
+
* Returns true if event was emitted.
|
|
184
|
+
*/
|
|
185
|
+
export function emitEvent(
|
|
186
|
+
pi: { events: { emit: (name: string, payload: unknown) => void } },
|
|
187
|
+
eventName: string,
|
|
188
|
+
payload: unknown,
|
|
189
|
+
): boolean {
|
|
190
|
+
try {
|
|
191
|
+
pi.events.emit(eventName, payload);
|
|
192
|
+
return true;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|