pi-deck 0.1.4 → 0.1.6
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 +35 -0
- package/dist/server.js +213 -114
- package/dist/server.js.map +3 -3
- package/package.json +1 -1
- package/packages/client/dist/assets/MarkdownContent-wbm_GFDE.js +2 -0
- package/packages/client/dist/assets/index-BZ24LH6A.css +1 -0
- package/packages/client/dist/assets/index-sHanNU2G.js +437 -0
- package/packages/client/dist/index.html +3 -3
- package/packages/client/dist/assets/MarkdownContent-CJeDwkyO.js +0 -2
- package/packages/client/dist/assets/index-DoQPGtrm.js +0 -354
- package/packages/client/dist/assets/index-FJKlxeM7.css +0 -1
package/README.md
CHANGED
|
@@ -95,6 +95,41 @@ The script will: check for a clean working tree on `main`, bump the version in a
|
|
|
95
95
|
|
|
96
96
|
The published package includes a bundled server (`dist/server.js`) and the pre-built client SPA (`packages/client/dist/`). Workspace dependencies are inlined by esbuild; only runtime dependencies (`express`, `better-sqlite3`, etc.) are installed by npm.
|
|
97
97
|
|
|
98
|
+
## Job Management
|
|
99
|
+
|
|
100
|
+
Pi-deck integrates with Pi's job management system. Jobs are markdown files with YAML frontmatter that track work through phases (backlog → planning → ready → executing → review → complete).
|
|
101
|
+
|
|
102
|
+
### Configurable Job Locations
|
|
103
|
+
|
|
104
|
+
By default, jobs are stored in two locations:
|
|
105
|
+
- `~/.pi/agent/jobs/<workspace-name>/` (primary)
|
|
106
|
+
- `<workspace>/.pi/jobs/` (local)
|
|
107
|
+
|
|
108
|
+
You can configure custom job directories by creating a `.pi/jobs.json` file in your workspace root:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"locations": [
|
|
113
|
+
"~/.pi/agent/jobs/pi-deck",
|
|
114
|
+
".pi/jobs",
|
|
115
|
+
"./custom-jobs-folder",
|
|
116
|
+
"/absolute/path/to/jobs"
|
|
117
|
+
],
|
|
118
|
+
"defaultLocation": "~/.pi/agent/jobs/pi-deck"
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Configuration options:**
|
|
123
|
+
- `locations` (required): Array of directory paths to scan for jobs. Supports:
|
|
124
|
+
- Relative paths (resolved from workspace root): `"./jobs"`, `".pi/jobs"`
|
|
125
|
+
- Absolute paths: `"/Users/you/projects/jobs"`
|
|
126
|
+
- Home directory expansion: `"~/.pi/agent/jobs"`
|
|
127
|
+
- `defaultLocation` (optional): Where new jobs are created. If not specified, uses the first location in `locations`.
|
|
128
|
+
|
|
129
|
+
The configuration file is optional — without it, the default two-location behavior is preserved for backward compatibility.
|
|
130
|
+
|
|
131
|
+
See `.pi/jobs.json.example` for a template.
|
|
132
|
+
|
|
98
133
|
## Configuration
|
|
99
134
|
|
|
100
135
|
Create a config file at `~/.config/pi-deck/config.json`:
|
package/dist/server.js
CHANGED
|
@@ -24,8 +24,8 @@ __export(plan_service_exports, {
|
|
|
24
24
|
writePlan: () => writePlan
|
|
25
25
|
});
|
|
26
26
|
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync, readdirSync as readdirSync2 } from "fs";
|
|
27
|
-
import { join as join3, basename
|
|
28
|
-
import { homedir as
|
|
27
|
+
import { join as join3, basename, resolve as resolve2 } from "path";
|
|
28
|
+
import { homedir as homedir3 } from "os";
|
|
29
29
|
function parseFrontmatter(content) {
|
|
30
30
|
const frontmatter = {};
|
|
31
31
|
if (!content.startsWith("---")) {
|
|
@@ -88,7 +88,7 @@ function parseTasks(content) {
|
|
|
88
88
|
function parsePlan(filePath, content) {
|
|
89
89
|
const { frontmatter } = parseFrontmatter(content);
|
|
90
90
|
const tasks = parseTasks(content);
|
|
91
|
-
const fileName =
|
|
91
|
+
const fileName = basename(filePath, ".md");
|
|
92
92
|
let title = frontmatter.title;
|
|
93
93
|
if (!title) {
|
|
94
94
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
@@ -161,9 +161,9 @@ ${fmBlock}
|
|
|
161
161
|
---${content.slice(endIndex + 4)}`;
|
|
162
162
|
}
|
|
163
163
|
function getPlanDirectories(workspacePath) {
|
|
164
|
-
const workspaceName =
|
|
164
|
+
const workspaceName = basename(workspacePath);
|
|
165
165
|
const dirs = [];
|
|
166
|
-
const globalDir = join3(
|
|
166
|
+
const globalDir = join3(homedir3(), "plans", workspaceName);
|
|
167
167
|
dirs.push(globalDir);
|
|
168
168
|
const localDir = join3(workspacePath, ".pi", "plans");
|
|
169
169
|
dirs.push(localDir);
|
|
@@ -254,13 +254,16 @@ __export(job_service_exports, {
|
|
|
254
254
|
extractReviewSection: () => extractReviewSection,
|
|
255
255
|
getActiveJobStates: () => getActiveJobStates,
|
|
256
256
|
getJobDirectories: () => getJobDirectories,
|
|
257
|
+
getJobLocations: () => getJobLocations,
|
|
257
258
|
getNextPhase: () => getNextPhase,
|
|
258
259
|
getPreviousPhase: () => getPreviousPhase,
|
|
260
|
+
loadJobConfig: () => loadJobConfig,
|
|
259
261
|
parseJob: () => parseJob,
|
|
260
262
|
parseJobFrontmatter: () => parseJobFrontmatter,
|
|
261
263
|
parseTasks: () => parseTasks,
|
|
262
264
|
promoteJob: () => promoteJob,
|
|
263
265
|
readJob: () => readJob,
|
|
266
|
+
resolveLocationPath: () => resolveLocationPath,
|
|
264
267
|
setJobSessionId: () => setJobSessionId,
|
|
265
268
|
unarchiveJob: () => unarchiveJob,
|
|
266
269
|
updateJobFrontmatter: () => updateJobFrontmatter,
|
|
@@ -268,9 +271,55 @@ __export(job_service_exports, {
|
|
|
268
271
|
writeJob: () => writeJob
|
|
269
272
|
});
|
|
270
273
|
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3, mkdirSync, renameSync } from "fs";
|
|
271
|
-
import { join as join4, basename as
|
|
272
|
-
import { homedir as
|
|
274
|
+
import { join as join4, basename as basename2, resolve as resolve3, dirname as dirname2 } from "path";
|
|
275
|
+
import { homedir as homedir4 } from "os";
|
|
273
276
|
import YAML from "yaml";
|
|
277
|
+
function loadJobConfig(workspacePath) {
|
|
278
|
+
const configPath = join4(workspacePath, ".pi", "jobs.json");
|
|
279
|
+
if (!existsSync4(configPath)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
const rawContent = readFileSync3(configPath, "utf-8");
|
|
284
|
+
const parsed = JSON.parse(rawContent);
|
|
285
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
286
|
+
throw new Error("Configuration must be a JSON object");
|
|
287
|
+
}
|
|
288
|
+
if (!Array.isArray(parsed.locations) || parsed.locations.length === 0) {
|
|
289
|
+
throw new Error('Configuration must have a non-empty "locations" array');
|
|
290
|
+
}
|
|
291
|
+
for (const loc of parsed.locations) {
|
|
292
|
+
if (typeof loc !== "string") {
|
|
293
|
+
throw new Error(`Location must be a string, got ${typeof loc}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (parsed.defaultLocation !== void 0 && typeof parsed.defaultLocation !== "string") {
|
|
297
|
+
throw new Error(`defaultLocation must be a string, got ${typeof parsed.defaultLocation}`);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
locations: parsed.locations,
|
|
301
|
+
defaultLocation: parsed.defaultLocation
|
|
302
|
+
};
|
|
303
|
+
} catch (err) {
|
|
304
|
+
if (err instanceof SyntaxError) {
|
|
305
|
+
throw new Error(`Failed to parse ${configPath}: Invalid JSON`);
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function resolveLocationPath(location, workspacePath) {
|
|
311
|
+
if (typeof location !== "string") {
|
|
312
|
+
throw new Error(`Location must be a string, got ${typeof location}`);
|
|
313
|
+
}
|
|
314
|
+
let path = location;
|
|
315
|
+
if (path.startsWith("~/") || path === "~") {
|
|
316
|
+
path = join4(homedir4(), path.slice(1));
|
|
317
|
+
}
|
|
318
|
+
if (!path.startsWith("/")) {
|
|
319
|
+
path = resolve3(workspacePath, path);
|
|
320
|
+
}
|
|
321
|
+
return path;
|
|
322
|
+
}
|
|
274
323
|
function statusToPhase(status) {
|
|
275
324
|
switch (status) {
|
|
276
325
|
case "draft":
|
|
@@ -366,7 +415,7 @@ function parseJobFrontmatter(content) {
|
|
|
366
415
|
function parseJob(filePath, content) {
|
|
367
416
|
const { frontmatter } = parseJobFrontmatter(content);
|
|
368
417
|
const tasks = parseTasks(content);
|
|
369
|
-
const fileName =
|
|
418
|
+
const fileName = basename2(filePath, ".md");
|
|
370
419
|
let title = frontmatter.title;
|
|
371
420
|
if (!title) {
|
|
372
421
|
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
@@ -434,9 +483,23 @@ function updateTaskInContent2(content, lineNumber, done) {
|
|
|
434
483
|
return lines.join("\n");
|
|
435
484
|
}
|
|
436
485
|
function getJobDirectories(workspacePath) {
|
|
437
|
-
|
|
486
|
+
try {
|
|
487
|
+
const config2 = loadJobConfig(workspacePath);
|
|
488
|
+
if (config2) {
|
|
489
|
+
return config2.locations.map((loc) => {
|
|
490
|
+
const resolvedPath = resolveLocationPath(loc, workspacePath);
|
|
491
|
+
if (!existsSync4(resolvedPath)) {
|
|
492
|
+
console.warn(`[JobService] Configured job directory does not exist: ${resolvedPath}`);
|
|
493
|
+
}
|
|
494
|
+
return resolvedPath;
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
} catch (err) {
|
|
498
|
+
console.warn(`[JobService] Failed to load job configuration: ${err instanceof Error ? err.message : String(err)}`);
|
|
499
|
+
}
|
|
500
|
+
const workspaceName = basename2(workspacePath);
|
|
438
501
|
const dirs = [];
|
|
439
|
-
dirs.push(join4(
|
|
502
|
+
dirs.push(join4(homedir4(), ".pi", "agent", "jobs", workspaceName));
|
|
440
503
|
dirs.push(join4(workspacePath, ".pi", "jobs"));
|
|
441
504
|
return dirs;
|
|
442
505
|
}
|
|
@@ -490,11 +553,53 @@ function writeJob(jobPath, content) {
|
|
|
490
553
|
writeFileSync2(jobPath, content, "utf-8");
|
|
491
554
|
return parseJob(jobPath, content);
|
|
492
555
|
}
|
|
493
|
-
function createJob(workspacePath, title, description, tags) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
556
|
+
function createJob(workspacePath, title, description, tags, location) {
|
|
557
|
+
let config2 = null;
|
|
558
|
+
let jobsDir;
|
|
559
|
+
try {
|
|
560
|
+
config2 = loadJobConfig(workspacePath);
|
|
561
|
+
if (location) {
|
|
562
|
+
if (config2) {
|
|
563
|
+
const resolvedLocation = resolveLocationPath(location, workspacePath);
|
|
564
|
+
const availableLocations = config2.locations.map((loc) => resolveLocationPath(loc, workspacePath));
|
|
565
|
+
if (availableLocations.includes(resolvedLocation)) {
|
|
566
|
+
jobsDir = resolvedLocation;
|
|
567
|
+
} else {
|
|
568
|
+
throw new Error(`Location "${location}" is not in the configured job locations`);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
const defaultLocs = [
|
|
572
|
+
join4(homedir4(), ".pi", "agent", "jobs", basename2(workspacePath)),
|
|
573
|
+
join4(workspacePath, ".pi", "jobs")
|
|
574
|
+
];
|
|
575
|
+
if (defaultLocs.includes(location)) {
|
|
576
|
+
jobsDir = location;
|
|
577
|
+
} else {
|
|
578
|
+
throw new Error(`Location "${location}" is not available (no custom config)`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} else if (config2 && config2.defaultLocation) {
|
|
582
|
+
jobsDir = resolveLocationPath(config2.defaultLocation, workspacePath);
|
|
583
|
+
} else if (config2 && config2.locations.length > 0) {
|
|
584
|
+
jobsDir = resolveLocationPath(config2.locations[0], workspacePath);
|
|
585
|
+
} else {
|
|
586
|
+
const workspaceName = basename2(workspacePath);
|
|
587
|
+
jobsDir = join4(homedir4(), ".pi", "agent", "jobs", workspaceName);
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
if (err instanceof Error && err.message.includes("not available")) {
|
|
591
|
+
throw err;
|
|
592
|
+
}
|
|
593
|
+
console.warn(`[JobService] Failed to load job config for createJob: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
const workspaceName = basename2(workspacePath);
|
|
595
|
+
jobsDir = join4(homedir4(), ".pi", "agent", "jobs", workspaceName);
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
if (!existsSync4(jobsDir)) {
|
|
599
|
+
mkdirSync(jobsDir, { recursive: true });
|
|
600
|
+
}
|
|
601
|
+
} catch (err) {
|
|
602
|
+
throw new Error(`Failed to create job directory at ${jobsDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
498
603
|
}
|
|
499
604
|
const now = /* @__PURE__ */ new Date();
|
|
500
605
|
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
@@ -579,7 +684,7 @@ function getArchivedDir(jobDir) {
|
|
|
579
684
|
}
|
|
580
685
|
function archiveJob(jobPath) {
|
|
581
686
|
const dir = dirname2(jobPath);
|
|
582
|
-
const file =
|
|
687
|
+
const file = basename2(jobPath);
|
|
583
688
|
const archivedDir = getArchivedDir(dir);
|
|
584
689
|
if (!existsSync4(archivedDir)) {
|
|
585
690
|
mkdirSync(archivedDir, { recursive: true });
|
|
@@ -591,7 +696,7 @@ function archiveJob(jobPath) {
|
|
|
591
696
|
function unarchiveJob(jobPath) {
|
|
592
697
|
const archivedDir = dirname2(jobPath);
|
|
593
698
|
const parentDir = dirname2(archivedDir);
|
|
594
|
-
const file =
|
|
699
|
+
const file = basename2(jobPath);
|
|
595
700
|
const newPath = join4(parentDir, file);
|
|
596
701
|
renameSync(jobPath, newPath);
|
|
597
702
|
return newPath;
|
|
@@ -623,14 +728,22 @@ function discoverArchivedJobs(workspacePath) {
|
|
|
623
728
|
}
|
|
624
729
|
function buildJobSystemContext(workspacePath) {
|
|
625
730
|
const dirs = getJobDirectories(workspacePath);
|
|
731
|
+
const config2 = loadJobConfig(workspacePath);
|
|
626
732
|
const primaryDir = dirs[0];
|
|
627
733
|
const jobs = discoverJobs(workspacePath);
|
|
628
734
|
const jobLines = jobs.map((j) => ` - [${j.phase}] "${j.title}" \u2192 ${j.path}`).join("\n");
|
|
629
735
|
const jobListing = jobs.length > 0 ? `
|
|
630
736
|
Current jobs:
|
|
631
737
|
${jobLines}` : "\nNo jobs exist yet.";
|
|
738
|
+
const dirListing = dirs.map((d) => ` - ${d}`).join("\n");
|
|
632
739
|
return `<job_system>
|
|
633
|
-
You have access to a job management system. Jobs are markdown files with YAML frontmatter
|
|
740
|
+
You have access to a job management system. Jobs are markdown files with YAML frontmatter.
|
|
741
|
+
|
|
742
|
+
## Job Locations
|
|
743
|
+
Jobs are stored in the following directories:
|
|
744
|
+
${dirListing}
|
|
745
|
+
|
|
746
|
+
${config2 ? `Configuration loaded from: .pi/jobs.json` : "Using default job locations (no .pi/jobs.json config)"}
|
|
634
747
|
|
|
635
748
|
## Job File Format
|
|
636
749
|
\`\`\`markdown
|
|
@@ -660,7 +773,7 @@ What needs to be done.
|
|
|
660
773
|
|
|
661
774
|
## Managing Jobs
|
|
662
775
|
- **Create a job**: Write a new .md file to ${primaryDir} with the frontmatter format above. Use filename format: YYYYMMDD-slug.md
|
|
663
|
-
- **List jobs**: Read files from
|
|
776
|
+
- **List jobs**: Read files from any of the job directories
|
|
664
777
|
- **Update phase**: Edit the \`phase\` field in frontmatter. Update \`updated\` timestamp.
|
|
665
778
|
- **Check off tasks**: Change \`- [ ]\` to \`- [x]\` in the job file.
|
|
666
779
|
- **Complete a job**: Set phase to "complete" and add \`completedAt\` to frontmatter.
|
|
@@ -675,7 +788,19 @@ function buildPlanningPrompt(jobPath) {
|
|
|
675
788
|
return `<active_job phase="planning">
|
|
676
789
|
You have a job to plan at: ${jobPath}
|
|
677
790
|
Read the job file. It contains a title and description.
|
|
791
|
+
|
|
792
|
+
Before creating the plan, you MUST:
|
|
793
|
+
1. Explore the codebase to understand the current implementation
|
|
794
|
+
2. Search for relevant files, functions, and existing patterns
|
|
795
|
+
3. Read documentation and configuration files as needed
|
|
796
|
+
4. Gather context about the architecture and conventions used
|
|
797
|
+
|
|
798
|
+
Do this research yourself \u2014 DO NOT include research or exploration tasks in the plan. The plan should only contain concrete implementation steps that will be performed after planning is complete.
|
|
799
|
+
|
|
678
800
|
Your goal is to create a detailed implementation plan. Ask the user clarifying questions if needed, then write a concrete plan with \`- [ ]\` checkbox tasks back into the job file under a "## Plan" section.
|
|
801
|
+
|
|
802
|
+
Plan tasks should be actionable implementation steps only (e.g., "Add function X", "Update file Y"). Do not include research tasks like "review current implementation" \u2014 you should do that during planning, not put it in the plan.
|
|
803
|
+
|
|
679
804
|
Group tasks under \`### Phase\` headings. Keep tasks concise and actionable (start with a verb).
|
|
680
805
|
When you're done writing the plan, let the user know so they can review and iterate or mark it as ready.
|
|
681
806
|
</active_job>`;
|
|
@@ -699,6 +824,24 @@ function extractReviewSection(content) {
|
|
|
699
824
|
const trimmed = sectionText.trim();
|
|
700
825
|
return trimmed.length > 0 ? trimmed : null;
|
|
701
826
|
}
|
|
827
|
+
function getJobLocations(workspacePath) {
|
|
828
|
+
const config2 = loadJobConfig(workspacePath);
|
|
829
|
+
const dirs = getJobDirectories(workspacePath);
|
|
830
|
+
let defaultDir;
|
|
831
|
+
if (config2 && config2.defaultLocation) {
|
|
832
|
+
defaultDir = resolveLocationPath(config2.defaultLocation, workspacePath);
|
|
833
|
+
} else if (config2 && config2.locations.length > 0) {
|
|
834
|
+
defaultDir = resolveLocationPath(config2.locations[0], workspacePath);
|
|
835
|
+
} else {
|
|
836
|
+
defaultDir = dirs[0];
|
|
837
|
+
}
|
|
838
|
+
return dirs.map((dir) => ({
|
|
839
|
+
path: dir,
|
|
840
|
+
isDefault: dir === defaultDir,
|
|
841
|
+
displayName: dir.startsWith(workspacePath) ? `.${dir.slice(workspacePath.length)}` : dir.replace(homedir4(), "~")
|
|
842
|
+
// Replace home directory with ~
|
|
843
|
+
}));
|
|
844
|
+
}
|
|
702
845
|
function buildReviewPrompt(jobPath) {
|
|
703
846
|
const content = readFileSync3(jobPath, "utf-8");
|
|
704
847
|
const reviewSection = extractReviewSection(content);
|
|
@@ -764,10 +907,10 @@ import { createServer } from "http";
|
|
|
764
907
|
import { WebSocketServer, WebSocket } from "ws";
|
|
765
908
|
import { fileURLToPath } from "url";
|
|
766
909
|
import { dirname as dirname5, join as join7, resolve as resolve4, sep } from "path";
|
|
767
|
-
import { existsSync as existsSync9, readFileSync as readFileSync4,
|
|
910
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, readdirSync as readdirSync5, statSync as statSync3 } from "fs";
|
|
768
911
|
import { unlink } from "fs/promises";
|
|
769
912
|
import { spawn } from "child_process";
|
|
770
|
-
import { homedir as
|
|
913
|
+
import { homedir as homedir6 } from "os";
|
|
771
914
|
import { SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
|
|
772
915
|
|
|
773
916
|
// packages/server/dist/config.js
|
|
@@ -801,9 +944,7 @@ function findProjectRoot(startDir) {
|
|
|
801
944
|
var projectRoot = findProjectRoot(process.cwd());
|
|
802
945
|
var DEFAULT_CONFIG = {
|
|
803
946
|
port: 9741,
|
|
804
|
-
host: "0.0.0.0"
|
|
805
|
-
// Default to detected project root, then home as fallback
|
|
806
|
-
allowedDirectories: [projectRoot, homedir()]
|
|
947
|
+
host: "0.0.0.0"
|
|
807
948
|
};
|
|
808
949
|
var CONFIG_PATHS = [
|
|
809
950
|
resolve(process.cwd(), "pi-deck.config.json"),
|
|
@@ -833,9 +974,6 @@ function loadConfigFromEnv() {
|
|
|
833
974
|
if (process.env.HOST) {
|
|
834
975
|
config2.host = process.env.HOST;
|
|
835
976
|
}
|
|
836
|
-
if (process.env.PI_ALLOWED_DIRS) {
|
|
837
|
-
config2.allowedDirectories = process.env.PI_ALLOWED_DIRS.split(":").map((d) => resolve(d.replace(/^~/, homedir())));
|
|
838
|
-
}
|
|
839
977
|
return config2;
|
|
840
978
|
}
|
|
841
979
|
var TILDE_PREFIX = /^~(?=\/|$)/;
|
|
@@ -847,57 +985,36 @@ function canonicalizePath(input) {
|
|
|
847
985
|
return normalized;
|
|
848
986
|
}
|
|
849
987
|
}
|
|
850
|
-
function normalizeDirectories(dirs) {
|
|
851
|
-
return dirs.map((d) => canonicalizePath(d));
|
|
852
|
-
}
|
|
853
988
|
function loadConfig() {
|
|
854
989
|
const fileConfig = loadConfigFromFile();
|
|
855
990
|
const envConfig = loadConfigFromEnv();
|
|
856
991
|
const config2 = {
|
|
857
992
|
port: envConfig.port ?? fileConfig.port ?? DEFAULT_CONFIG.port,
|
|
858
|
-
host: envConfig.host ?? fileConfig.host ?? DEFAULT_CONFIG.host
|
|
859
|
-
allowedDirectories: normalizeDirectories(envConfig.allowedDirectories ?? fileConfig.allowedDirectories ?? DEFAULT_CONFIG.allowedDirectories)
|
|
993
|
+
host: envConfig.host ?? fileConfig.host ?? DEFAULT_CONFIG.host
|
|
860
994
|
};
|
|
861
|
-
console.log("[Config] Allowed directories:", config2.allowedDirectories);
|
|
862
995
|
return config2;
|
|
863
996
|
}
|
|
864
|
-
function isPathAllowed(path, allowedDirectories) {
|
|
865
|
-
const normalizedPath = canonicalizePath(path);
|
|
866
|
-
return allowedDirectories.some((allowed) => {
|
|
867
|
-
const normalizedAllowed = canonicalizePath(allowed);
|
|
868
|
-
if (normalizedAllowed === "/") {
|
|
869
|
-
return true;
|
|
870
|
-
}
|
|
871
|
-
return normalizedPath === normalizedAllowed || normalizedPath.startsWith(normalizedAllowed + "/");
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
997
|
|
|
875
998
|
// packages/server/dist/directory-browser.js
|
|
876
999
|
import { readdirSync, statSync, existsSync as existsSync2 } from "fs";
|
|
877
|
-
import {
|
|
1000
|
+
import { join as join2 } from "path";
|
|
1001
|
+
import { homedir as homedir2 } from "os";
|
|
878
1002
|
var DirectoryBrowser = class {
|
|
879
|
-
allowedDirectories;
|
|
880
|
-
constructor(allowedDirectories) {
|
|
881
|
-
this.allowedDirectories = allowedDirectories;
|
|
882
|
-
}
|
|
883
1003
|
/**
|
|
884
1004
|
* List the allowed root directories
|
|
885
1005
|
*/
|
|
886
1006
|
listRoots() {
|
|
887
|
-
return
|
|
888
|
-
name:
|
|
889
|
-
path:
|
|
890
|
-
hasPiSessions: this.checkForPiSessions(
|
|
891
|
-
}
|
|
1007
|
+
return [{
|
|
1008
|
+
name: "Home",
|
|
1009
|
+
path: homedir2(),
|
|
1010
|
+
hasPiSessions: this.checkForPiSessions(homedir2())
|
|
1011
|
+
}];
|
|
892
1012
|
}
|
|
893
1013
|
/**
|
|
894
1014
|
* Browse a directory and return its subdirectories
|
|
895
1015
|
*/
|
|
896
1016
|
browse(path) {
|
|
897
1017
|
const normalizedPath = canonicalizePath(path);
|
|
898
|
-
if (!isPathAllowed(normalizedPath, this.allowedDirectories)) {
|
|
899
|
-
throw new Error(`Access denied: ${path} is not within allowed directories`);
|
|
900
|
-
}
|
|
901
1018
|
if (!existsSync2(normalizedPath)) {
|
|
902
1019
|
throw new Error(`Directory not found: ${path}`);
|
|
903
1020
|
}
|
|
@@ -942,13 +1059,13 @@ var DirectoryBrowser = class {
|
|
|
942
1059
|
* Get the allowed directories
|
|
943
1060
|
*/
|
|
944
1061
|
getAllowedDirectories() {
|
|
945
|
-
return [
|
|
1062
|
+
return [homedir2()];
|
|
946
1063
|
}
|
|
947
1064
|
};
|
|
948
1065
|
|
|
949
1066
|
// packages/server/dist/workspace-manager.js
|
|
950
1067
|
import { EventEmitter as EventEmitter3 } from "events";
|
|
951
|
-
import { basename as
|
|
1068
|
+
import { basename as basename3 } from "path";
|
|
952
1069
|
|
|
953
1070
|
// packages/server/dist/session-orchestrator.js
|
|
954
1071
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
@@ -1927,7 +2044,9 @@ var PiSession = class extends EventEmitter {
|
|
|
1927
2044
|
cacheRead: msg.usage.cacheRead || 0,
|
|
1928
2045
|
cacheWrite: msg.usage.cacheWrite || 0,
|
|
1929
2046
|
total: (msg.usage.input || 0) + (msg.usage.output || 0)
|
|
1930
|
-
} : void 0
|
|
2047
|
+
} : void 0,
|
|
2048
|
+
stopReason: msg.stopReason,
|
|
2049
|
+
errorMessage: msg.errorMessage
|
|
1931
2050
|
};
|
|
1932
2051
|
}
|
|
1933
2052
|
convertContent(content) {
|
|
@@ -2975,14 +3094,12 @@ var SessionOrchestrator = class extends EventEmitter2 {
|
|
|
2975
3094
|
|
|
2976
3095
|
// packages/server/dist/workspace-manager.js
|
|
2977
3096
|
var WorkspaceManager = class extends EventEmitter3 {
|
|
2978
|
-
allowedDirectories;
|
|
2979
3097
|
workspaces = /* @__PURE__ */ new Map();
|
|
2980
3098
|
nextWorkspaceId = 1;
|
|
2981
3099
|
syncIntegration = null;
|
|
2982
3100
|
openingPaths = /* @__PURE__ */ new Set();
|
|
2983
|
-
constructor(
|
|
3101
|
+
constructor() {
|
|
2984
3102
|
super();
|
|
2985
|
-
this.allowedDirectories = allowedDirectories;
|
|
2986
3103
|
}
|
|
2987
3104
|
/**
|
|
2988
3105
|
* Set the sync integration for state tracking
|
|
@@ -2997,9 +3114,6 @@ var WorkspaceManager = class extends EventEmitter3 {
|
|
|
2997
3114
|
*/
|
|
2998
3115
|
async openWorkspace(path) {
|
|
2999
3116
|
const normalizedPath = canonicalizePath(path);
|
|
3000
|
-
if (!isPathAllowed(normalizedPath, this.allowedDirectories)) {
|
|
3001
|
-
throw new Error(`Access denied: ${normalizedPath} is not within allowed directories`);
|
|
3002
|
-
}
|
|
3003
3117
|
while (this.openingPaths.has(normalizedPath)) {
|
|
3004
3118
|
await new Promise((resolve5) => setTimeout(resolve5, 10));
|
|
3005
3119
|
}
|
|
@@ -3044,7 +3158,7 @@ var WorkspaceManager = class extends EventEmitter3 {
|
|
|
3044
3158
|
const workspace = {
|
|
3045
3159
|
id,
|
|
3046
3160
|
path: normalizedPath,
|
|
3047
|
-
name:
|
|
3161
|
+
name: basename3(normalizedPath) || normalizedPath,
|
|
3048
3162
|
orchestrator,
|
|
3049
3163
|
unsubscribe,
|
|
3050
3164
|
clientCount: 1,
|
|
@@ -3258,9 +3372,9 @@ var WorkspaceManager = class extends EventEmitter3 {
|
|
|
3258
3372
|
}
|
|
3259
3373
|
};
|
|
3260
3374
|
var workspaceManager = null;
|
|
3261
|
-
function getWorkspaceManager(
|
|
3375
|
+
function getWorkspaceManager() {
|
|
3262
3376
|
if (!workspaceManager) {
|
|
3263
|
-
workspaceManager = new WorkspaceManager(
|
|
3377
|
+
workspaceManager = new WorkspaceManager();
|
|
3264
3378
|
}
|
|
3265
3379
|
return workspaceManager;
|
|
3266
3380
|
}
|
|
@@ -3268,7 +3382,7 @@ function getWorkspaceManager(allowedDirectories) {
|
|
|
3268
3382
|
// packages/server/dist/ui-state.js
|
|
3269
3383
|
import Database from "better-sqlite3";
|
|
3270
3384
|
import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
|
|
3271
|
-
import { homedir as
|
|
3385
|
+
import { homedir as homedir5 } from "os";
|
|
3272
3386
|
import { dirname as dirname3, join as join5 } from "path";
|
|
3273
3387
|
var DEFAULT_STATE = {
|
|
3274
3388
|
openWorkspaces: [],
|
|
@@ -3287,7 +3401,7 @@ var DEFAULT_STATE = {
|
|
|
3287
3401
|
var UIStateStore = class {
|
|
3288
3402
|
db;
|
|
3289
3403
|
constructor(dbPath) {
|
|
3290
|
-
const path = dbPath || join5(
|
|
3404
|
+
const path = dbPath || join5(homedir5(), ".config", "pi-deck", "ui-state.db");
|
|
3291
3405
|
const dir = dirname3(path);
|
|
3292
3406
|
if (!existsSync6(dir)) {
|
|
3293
3407
|
mkdirSync2(dir, { recursive: true });
|
|
@@ -5179,7 +5293,7 @@ var SyncIntegration = class extends EventEmitter8 {
|
|
|
5179
5293
|
|
|
5180
5294
|
// packages/server/dist/index.js
|
|
5181
5295
|
var config = loadConfig();
|
|
5182
|
-
var syncDbPath = join7(
|
|
5296
|
+
var syncDbPath = join7(homedir6(), ".pi", "pi-deck-sync.db");
|
|
5183
5297
|
var syncIntegration = new SyncIntegration(syncDbPath);
|
|
5184
5298
|
console.log(`[Sync] Initialized sync database at ${syncDbPath}`);
|
|
5185
5299
|
var PORT = config.port;
|
|
@@ -5219,9 +5333,9 @@ if (existsSync9(clientDistPath)) {
|
|
|
5219
5333
|
}
|
|
5220
5334
|
var server = createServer(app);
|
|
5221
5335
|
var wss = new WebSocketServer({ server, path: "/ws" });
|
|
5222
|
-
var directoryBrowser = new DirectoryBrowser(
|
|
5336
|
+
var directoryBrowser = new DirectoryBrowser();
|
|
5223
5337
|
var uiStateStore = getUIStateStore();
|
|
5224
|
-
var workspaceManager2 = getWorkspaceManager(
|
|
5338
|
+
var workspaceManager2 = getWorkspaceManager();
|
|
5225
5339
|
workspaceManager2.setSyncIntegration(syncIntegration);
|
|
5226
5340
|
var clientWorkspaces = /* @__PURE__ */ new Map();
|
|
5227
5341
|
var pendingQuestionnaireRoutes = /* @__PURE__ */ new Map();
|
|
@@ -5360,7 +5474,6 @@ Please read the job file and execute the review steps.`;
|
|
|
5360
5474
|
app.get("/health", (_req, res) => {
|
|
5361
5475
|
res.json({
|
|
5362
5476
|
status: "ok",
|
|
5363
|
-
allowedDirectories: config.allowedDirectories,
|
|
5364
5477
|
activeWorkspaces: workspaceManager2.listWorkspaces().length
|
|
5365
5478
|
});
|
|
5366
5479
|
});
|
|
@@ -5372,8 +5485,7 @@ wss.on("connection", async (ws) => {
|
|
|
5372
5485
|
send(ws, {
|
|
5373
5486
|
type: "connected",
|
|
5374
5487
|
workspaces: existingWorkspaces,
|
|
5375
|
-
|
|
5376
|
-
homeDirectory: homedir5(),
|
|
5488
|
+
homeDirectory: homedir6(),
|
|
5377
5489
|
uiState,
|
|
5378
5490
|
...updateAvailable ? { updateAvailable } : {}
|
|
5379
5491
|
});
|
|
@@ -5520,8 +5632,7 @@ async function handleMessage(ws, message) {
|
|
|
5520
5632
|
send(ws, {
|
|
5521
5633
|
type: "directoryList",
|
|
5522
5634
|
path: "/",
|
|
5523
|
-
entries: directoryBrowser.listRoots()
|
|
5524
|
-
allowedRoots: directoryBrowser.getAllowedDirectories()
|
|
5635
|
+
entries: directoryBrowser.listRoots()
|
|
5525
5636
|
});
|
|
5526
5637
|
}
|
|
5527
5638
|
break;
|
|
@@ -6183,26 +6294,6 @@ ${planNote}` : planNote;
|
|
|
6183
6294
|
});
|
|
6184
6295
|
break;
|
|
6185
6296
|
}
|
|
6186
|
-
case "updateAllowedRoots": {
|
|
6187
|
-
const { roots } = message;
|
|
6188
|
-
console.log("[Config] Updating allowed roots:", roots);
|
|
6189
|
-
const configPath = join7(homedir5(), ".pi-deck.json");
|
|
6190
|
-
let fileConfig = {};
|
|
6191
|
-
try {
|
|
6192
|
-
if (existsSync9(configPath)) {
|
|
6193
|
-
fileConfig = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
6194
|
-
}
|
|
6195
|
-
} catch {
|
|
6196
|
-
}
|
|
6197
|
-
fileConfig.allowedDirectories = roots;
|
|
6198
|
-
writeFileSync3(configPath, JSON.stringify(fileConfig, null, 2));
|
|
6199
|
-
console.log("[Config] Saved config to", configPath);
|
|
6200
|
-
send(ws, {
|
|
6201
|
-
type: "allowedRootsUpdated",
|
|
6202
|
-
roots
|
|
6203
|
-
});
|
|
6204
|
-
break;
|
|
6205
|
-
}
|
|
6206
6297
|
// ========================================================================
|
|
6207
6298
|
// Session Tree Navigation
|
|
6208
6299
|
// ========================================================================
|
|
@@ -6433,26 +6524,13 @@ ${planNote}` : planNote;
|
|
|
6433
6524
|
}
|
|
6434
6525
|
const rootPath = resolve4(workspace.path);
|
|
6435
6526
|
const rawPath = message.path || "";
|
|
6436
|
-
const expandedPath = rawPath.startsWith("~/") ? join7(
|
|
6527
|
+
const expandedPath = rawPath.startsWith("~/") ? join7(homedir6(), rawPath.slice(2)) : rawPath;
|
|
6437
6528
|
const isAbsolute = expandedPath.startsWith("/");
|
|
6438
6529
|
let targetPath;
|
|
6439
6530
|
let displayPath;
|
|
6440
6531
|
if (isAbsolute) {
|
|
6441
6532
|
targetPath = resolve4(expandedPath);
|
|
6442
6533
|
displayPath = rawPath;
|
|
6443
|
-
const inWorkspace = targetPath.startsWith(rootPath + sep) || targetPath === rootPath;
|
|
6444
|
-
const inAllowed = config.allowedDirectories.some((dir) => targetPath.startsWith(resolve4(dir) + sep) || targetPath === resolve4(dir));
|
|
6445
|
-
if (!inWorkspace && !inAllowed) {
|
|
6446
|
-
send(ws, {
|
|
6447
|
-
type: "workspaceFile",
|
|
6448
|
-
workspaceId: message.workspaceId,
|
|
6449
|
-
path: displayPath,
|
|
6450
|
-
content: "",
|
|
6451
|
-
truncated: false,
|
|
6452
|
-
requestId: message.requestId
|
|
6453
|
-
});
|
|
6454
|
-
break;
|
|
6455
|
-
}
|
|
6456
6534
|
} else {
|
|
6457
6535
|
const relativePath = rawPath.replace(/^\/+/, "");
|
|
6458
6536
|
targetPath = resolve4(rootPath, relativePath);
|
|
@@ -6930,6 +7008,28 @@ Please read the plan file and begin working through the tasks.`;
|
|
|
6930
7008
|
syncIntegration.setJobs(message.workspaceId, jobs);
|
|
6931
7009
|
break;
|
|
6932
7010
|
}
|
|
7011
|
+
case "getJobLocations": {
|
|
7012
|
+
const workspace = workspaceManager2.getWorkspace(message.workspaceId);
|
|
7013
|
+
if (!workspace)
|
|
7014
|
+
break;
|
|
7015
|
+
try {
|
|
7016
|
+
const locations = getJobLocations(workspace.path);
|
|
7017
|
+
const defaultLocation = locations.find((l) => l.isDefault)?.path || locations[0]?.path;
|
|
7018
|
+
send(ws, {
|
|
7019
|
+
type: "jobLocations",
|
|
7020
|
+
workspaceId: message.workspaceId,
|
|
7021
|
+
locations,
|
|
7022
|
+
defaultLocation
|
|
7023
|
+
});
|
|
7024
|
+
} catch (err) {
|
|
7025
|
+
send(ws, {
|
|
7026
|
+
type: "error",
|
|
7027
|
+
message: `Failed to get job locations: ${err instanceof Error ? err.message : "Unknown error"}`,
|
|
7028
|
+
workspaceId: message.workspaceId
|
|
7029
|
+
});
|
|
7030
|
+
}
|
|
7031
|
+
break;
|
|
7032
|
+
}
|
|
6933
7033
|
case "getJobContent": {
|
|
6934
7034
|
const workspace = workspaceManager2.getWorkspace(message.workspaceId);
|
|
6935
7035
|
if (!workspace)
|
|
@@ -6957,7 +7057,7 @@ Please read the plan file and begin working through the tasks.`;
|
|
|
6957
7057
|
if (!workspace)
|
|
6958
7058
|
break;
|
|
6959
7059
|
try {
|
|
6960
|
-
const { path: jobPath, job } = createJob(workspace.path, message.title, message.description, message.tags);
|
|
7060
|
+
const { path: jobPath, job } = createJob(workspace.path, message.title, message.description, message.tags, message.location);
|
|
6961
7061
|
broadcastToWorkspace(message.workspaceId, {
|
|
6962
7062
|
type: "jobSaved",
|
|
6963
7063
|
workspaceId: message.workspaceId,
|
|
@@ -7319,7 +7419,6 @@ if (existsSync9(clientDistPath)) {
|
|
|
7319
7419
|
}
|
|
7320
7420
|
server.listen(PORT, config.host, () => {
|
|
7321
7421
|
console.log(`[Server] Pi-Deck server running on http://${config.host}:${PORT}`);
|
|
7322
|
-
console.log(`[Server] Allowed directories: ${config.allowedDirectories.join(", ")}`);
|
|
7323
7422
|
console.log(`[Server] WebSocket endpoint: ws://${config.host}:${PORT}/ws`);
|
|
7324
7423
|
});
|
|
7325
7424
|
//# sourceMappingURL=server.js.map
|