paseo-form-render 0.0.0-stage → 0.2.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 +115 -2
- package/client/container.tsx +289 -0
- package/client/host-markdown.tsx +24 -0
- package/client/markdown.tsx +470 -0
- package/client/messages.ts +147 -0
- package/client/project-file-modal.tsx +690 -0
- package/client/react-native-dataset.d.ts +10 -0
- package/client/renderer.tsx +1056 -0
- package/client/rpc-bridge.tsx +27 -0
- package/client/settings-screen.tsx +163 -0
- package/index.client.tsx +103 -0
- package/index.server.ts +38 -0
- package/package.json +50 -4
- package/paseo-plugin.json +8 -0
- package/server/auto-fix.ts +217 -0
- package/server/demo.ts +20 -0
- package/server/file-picker.ts +253 -0
- package/server/settings.ts +26 -0
- package/shared/demo.ts +96 -0
- package/shared/form-errors.ts +390 -0
- package/shared/form-fix.ts +77 -0
- package/shared/form-helpers.ts +214 -0
- package/shared/parser.ts +119 -0
- package/shared/pickers.ts +50 -0
- package/shared/prompt.ts +96 -0
- package/shared/schema.ts +330 -0
- package/shared/settings.ts +18 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import type { RpcInput } from "@getpaseo/plugin";
|
|
5
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
6
|
+
import {
|
|
7
|
+
type listProjectFilesRpc,
|
|
8
|
+
type createDirectoryRpc,
|
|
9
|
+
type FilePickerShortcut,
|
|
10
|
+
} from "../shared/pickers";
|
|
11
|
+
|
|
12
|
+
type Paseo = Parameters<Parameters<PluginServerContext["handle"]>[1]>[1]["paseo"];
|
|
13
|
+
|
|
14
|
+
export async function handleListProjectFiles(
|
|
15
|
+
{ agentId, subPath, targetPath }: RpcInput<typeof listProjectFilesRpc>,
|
|
16
|
+
{ paseo }: { paseo: Paseo },
|
|
17
|
+
) {
|
|
18
|
+
try {
|
|
19
|
+
const agent = paseo.agents.ref(agentId);
|
|
20
|
+
let cwd: string | null | undefined = agent?.cwd;
|
|
21
|
+
if (!cwd) {
|
|
22
|
+
if (agent) {
|
|
23
|
+
const snapshot = await agent.refresh();
|
|
24
|
+
cwd = snapshot?.agent?.cwd;
|
|
25
|
+
}
|
|
26
|
+
if (!cwd) {
|
|
27
|
+
cwd = process.cwd();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const homeDir = os.homedir();
|
|
32
|
+
const shortcuts: FilePickerShortcut[] = [
|
|
33
|
+
{ id: "project", label: "Project", icon: "📁", path: cwd },
|
|
34
|
+
{ id: "home", label: "Home", icon: "🏠", path: homeDir },
|
|
35
|
+
];
|
|
36
|
+
const desktopDir = path.join(homeDir, "Desktop");
|
|
37
|
+
if (fs.existsSync(desktopDir)) {
|
|
38
|
+
shortcuts.push({ id: "desktop", label: "Desktop", icon: "🖥️", path: desktopDir });
|
|
39
|
+
}
|
|
40
|
+
const downloadsDir = path.join(homeDir, "Downloads");
|
|
41
|
+
if (fs.existsSync(downloadsDir)) {
|
|
42
|
+
shortcuts.push({ id: "downloads", label: "Downloads", icon: "📥", path: downloadsDir });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// OneDrive detection: check Windows env vars (OneDrive, OneDriveCommercial, OneDriveConsumer) or ~/OneDrive
|
|
46
|
+
const oneDriveCandidates = [
|
|
47
|
+
{ id: "onedrive", label: "OneDrive", path: process.env.OneDrive },
|
|
48
|
+
{ id: "onedrive-commercial", label: "OneDrive (Work)", path: process.env.OneDriveCommercial },
|
|
49
|
+
{ id: "onedrive-consumer", label: "OneDrive (Personal)", path: process.env.OneDriveConsumer },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const addedOneDrivePaths = new Set<string>();
|
|
53
|
+
for (const item of oneDriveCandidates) {
|
|
54
|
+
if (item.path && !addedOneDrivePaths.has(item.path)) {
|
|
55
|
+
try {
|
|
56
|
+
if (fs.existsSync(item.path)) {
|
|
57
|
+
addedOneDrivePaths.add(item.path);
|
|
58
|
+
shortcuts.push({
|
|
59
|
+
id: item.id,
|
|
60
|
+
label: item.label,
|
|
61
|
+
icon: "☁️",
|
|
62
|
+
path: item.path,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (addedOneDrivePaths.size === 0) {
|
|
70
|
+
const defaultOneDrive = path.join(homeDir, "OneDrive");
|
|
71
|
+
try {
|
|
72
|
+
if (fs.existsSync(defaultOneDrive)) {
|
|
73
|
+
shortcuts.push({
|
|
74
|
+
id: "onedrive",
|
|
75
|
+
label: "OneDrive",
|
|
76
|
+
icon: "☁️",
|
|
77
|
+
path: defaultOneDrive,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let rawPath = (targetPath !== undefined && targetPath !== null && targetPath !== "")
|
|
84
|
+
? targetPath
|
|
85
|
+
: (subPath || "");
|
|
86
|
+
|
|
87
|
+
let targetDir: string;
|
|
88
|
+
if (rawPath.startsWith("~")) {
|
|
89
|
+
targetDir = path.resolve(homeDir, rawPath.slice(1).replace(/^[/\\]/, ""));
|
|
90
|
+
} else if (path.isAbsolute(rawPath)) {
|
|
91
|
+
targetDir = path.resolve(rawPath);
|
|
92
|
+
} else if (rawPath) {
|
|
93
|
+
targetDir = path.resolve(cwd, rawPath);
|
|
94
|
+
} else {
|
|
95
|
+
targetDir = cwd;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const stat = await fs.promises.stat(targetDir);
|
|
100
|
+
if (!stat.isDirectory()) {
|
|
101
|
+
targetDir = path.dirname(targetDir);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
targetDir = cwd;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const relToCwd = path.relative(cwd, targetDir);
|
|
108
|
+
const isWorkspaceRelative =
|
|
109
|
+
!relToCwd.startsWith("..") && !path.isAbsolute(relToCwd);
|
|
110
|
+
|
|
111
|
+
const currentPath = isWorkspaceRelative
|
|
112
|
+
? (relToCwd ? relToCwd.replace(/\\/g, "/") : "")
|
|
113
|
+
: targetDir.replace(/\\/g, "/");
|
|
114
|
+
|
|
115
|
+
let parentPath: string | null = null;
|
|
116
|
+
if (isWorkspaceRelative) {
|
|
117
|
+
if (currentPath) {
|
|
118
|
+
const parentResolved = path.dirname(targetDir);
|
|
119
|
+
const relParent = path.relative(cwd, parentResolved).replace(/\\/g, "/");
|
|
120
|
+
parentPath = relParent === "" ? "" : relParent;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
const parentDir = path.dirname(targetDir);
|
|
124
|
+
if (parentDir !== targetDir) {
|
|
125
|
+
parentPath = parentDir.replace(/\\/g, "/");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let entries: { name: string; isDirectory: boolean; relativePath: string; fullPath: string }[] = [];
|
|
130
|
+
let readError: string | undefined;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const dirents = await fs.promises.readdir(targetDir, { withFileTypes: true });
|
|
134
|
+
entries = dirents
|
|
135
|
+
.filter((d) => !d.name.startsWith(".") && d.name !== "node_modules")
|
|
136
|
+
.map((d) => {
|
|
137
|
+
const fullPath = path.join(targetDir, d.name);
|
|
138
|
+
const entryRelToCwd = path.relative(cwd, fullPath);
|
|
139
|
+
const entryInsideCwd =
|
|
140
|
+
!entryRelToCwd.startsWith("..") && !path.isAbsolute(entryRelToCwd);
|
|
141
|
+
const relativePath = entryInsideCwd
|
|
142
|
+
? entryRelToCwd.replace(/\\/g, "/")
|
|
143
|
+
: fullPath.replace(/\\/g, "/");
|
|
144
|
+
return {
|
|
145
|
+
name: d.name,
|
|
146
|
+
isDirectory: d.isDirectory(),
|
|
147
|
+
relativePath,
|
|
148
|
+
fullPath: fullPath.replace(/\\/g, "/"),
|
|
149
|
+
};
|
|
150
|
+
})
|
|
151
|
+
.sort((a, b) => {
|
|
152
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
153
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
154
|
+
return a.name.localeCompare(b.name);
|
|
155
|
+
});
|
|
156
|
+
} catch (e) {
|
|
157
|
+
readError = e instanceof Error ? e.message : String(e);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
currentPath,
|
|
162
|
+
parentPath,
|
|
163
|
+
entries,
|
|
164
|
+
shortcuts,
|
|
165
|
+
isWorkspaceRelative,
|
|
166
|
+
error: readError,
|
|
167
|
+
};
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return {
|
|
170
|
+
currentPath: "",
|
|
171
|
+
parentPath: null,
|
|
172
|
+
entries: [],
|
|
173
|
+
error: err instanceof Error ? err.message : String(err),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function handleCreateDirectory(
|
|
179
|
+
{ agentId, parentPath, name }: RpcInput<typeof createDirectoryRpc>,
|
|
180
|
+
{ paseo }: { paseo: Paseo },
|
|
181
|
+
) {
|
|
182
|
+
try {
|
|
183
|
+
const trimmedName = name.trim();
|
|
184
|
+
if (!trimmedName) {
|
|
185
|
+
return { success: false, error: "Folder name cannot be empty." };
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
trimmedName === "." ||
|
|
189
|
+
trimmedName === ".." ||
|
|
190
|
+
trimmedName.includes("/") ||
|
|
191
|
+
trimmedName.includes("\\") ||
|
|
192
|
+
/[<>:"|?*]/.test(trimmedName)
|
|
193
|
+
) {
|
|
194
|
+
return { success: false, error: "Folder name contains invalid characters." };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const agent = paseo.agents.ref(agentId);
|
|
198
|
+
let cwd: string | null | undefined = agent?.cwd;
|
|
199
|
+
if (!cwd) {
|
|
200
|
+
if (agent) {
|
|
201
|
+
const snapshot = await agent.refresh();
|
|
202
|
+
cwd = snapshot?.agent?.cwd;
|
|
203
|
+
}
|
|
204
|
+
if (!cwd) {
|
|
205
|
+
cwd = process.cwd();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const homeDir = os.homedir();
|
|
210
|
+
let targetDir: string;
|
|
211
|
+
if (parentPath.startsWith("~")) {
|
|
212
|
+
targetDir = path.resolve(homeDir, parentPath.slice(1).replace(/^[/\\]/, ""));
|
|
213
|
+
} else if (path.isAbsolute(parentPath)) {
|
|
214
|
+
targetDir = path.resolve(parentPath);
|
|
215
|
+
} else if (parentPath) {
|
|
216
|
+
targetDir = path.resolve(cwd, parentPath);
|
|
217
|
+
} else {
|
|
218
|
+
targetDir = cwd;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const newFullPath = path.join(targetDir, trimmedName);
|
|
222
|
+
|
|
223
|
+
if (path.dirname(newFullPath) !== targetDir) {
|
|
224
|
+
return { success: false, error: "Invalid target path." };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (fs.existsSync(newFullPath)) {
|
|
228
|
+
return { success: false, error: "A folder or file with this name already exists." };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await fs.promises.mkdir(newFullPath, { recursive: false });
|
|
232
|
+
|
|
233
|
+
const relToCwd = path.relative(cwd, newFullPath);
|
|
234
|
+
const isWorkspaceRelative =
|
|
235
|
+
!relToCwd.startsWith("..") && !path.isAbsolute(relToCwd);
|
|
236
|
+
|
|
237
|
+
const relativePath = isWorkspaceRelative
|
|
238
|
+
? (relToCwd ? relToCwd.replace(/\\/g, "/") : "")
|
|
239
|
+
: newFullPath.replace(/\\/g, "/");
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
fullPath: newFullPath.replace(/\\/g, "/"),
|
|
244
|
+
relativePath,
|
|
245
|
+
};
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
if (err?.code === "EACCES" || err?.code === "EPERM") {
|
|
248
|
+
return { success: false, error: "Permission denied: unable to create folder in this location." };
|
|
249
|
+
}
|
|
250
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { DEFAULT_AUTO_FIX_RETRIES, DEFAULT_PREFER_FORMS, pfrSettings } from "../shared/settings";
|
|
3
|
+
|
|
4
|
+
export function registerPfrSettings(server: PluginServerContext) {
|
|
5
|
+
return server.registerSettings(pfrSettings);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type PfrSettingsHandle = ReturnType<typeof registerPfrSettings>;
|
|
9
|
+
|
|
10
|
+
export interface PfrSettingsValues {
|
|
11
|
+
autoFixMaxRetries: number;
|
|
12
|
+
preferForms: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_VALUES: PfrSettingsValues = { autoFixMaxRetries: DEFAULT_AUTO_FIX_RETRIES, preferForms: DEFAULT_PREFER_FORMS };
|
|
16
|
+
|
|
17
|
+
/** Reads the saved settings, falling back to the defaults when they cannot be loaded. */
|
|
18
|
+
export async function readPfrSettings(settings: PfrSettingsHandle): Promise<PfrSettingsValues> {
|
|
19
|
+
try {
|
|
20
|
+
const state = await settings.read();
|
|
21
|
+
if (state.status === "ready") return state.values;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.warn("[paseo-form-render] could not read settings, using defaults", error);
|
|
24
|
+
}
|
|
25
|
+
return DEFAULT_VALUES;
|
|
26
|
+
}
|
package/shared/demo.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const TIMELINE_KIND = "paseo-form-render";
|
|
5
|
+
|
|
6
|
+
/** Asks the daemon to put the demo form into an agent's timeline; nothing is sent to the agent. */
|
|
7
|
+
export const showDemoForm = defineRpc({
|
|
8
|
+
name: "pfr-form.demo",
|
|
9
|
+
input: z.object({ agentId: z.string() }),
|
|
10
|
+
output: z.object({}),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const DEMO_FORM_TEXT = [
|
|
14
|
+
"Here is a demonstration of the interactive form:",
|
|
15
|
+
"```pfr-form",
|
|
16
|
+
JSON.stringify(
|
|
17
|
+
{
|
|
18
|
+
type: "Card",
|
|
19
|
+
title: "Demo Configuration Form",
|
|
20
|
+
description: "Try filling out this form and submitting it to the agent.",
|
|
21
|
+
children: [
|
|
22
|
+
{ type: "Metric", label: "Current Version", value: "v0.1.0" },
|
|
23
|
+
{
|
|
24
|
+
type: "Input",
|
|
25
|
+
id: "project_name",
|
|
26
|
+
label: "Project Name",
|
|
27
|
+
placeholder: "e.g., My Awesome Project",
|
|
28
|
+
required: true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: "Input",
|
|
32
|
+
id: "project_desc",
|
|
33
|
+
label: "Project Description",
|
|
34
|
+
placeholder: "Enter details (vertically resizable)...",
|
|
35
|
+
multiline: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "Select",
|
|
39
|
+
id: "environment",
|
|
40
|
+
label: "Target Environment",
|
|
41
|
+
options: [
|
|
42
|
+
{ label: "Development", value: "dev" },
|
|
43
|
+
{ label: "Staging", value: "staging" },
|
|
44
|
+
{ label: "Production", value: "prod" },
|
|
45
|
+
],
|
|
46
|
+
defaultValue: "dev",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: "MultiSelect",
|
|
50
|
+
id: "features",
|
|
51
|
+
label: "Included Features",
|
|
52
|
+
placeholder: "Select features to enable...",
|
|
53
|
+
options: [
|
|
54
|
+
{ label: "Authentication (SSO)", value: "auth" },
|
|
55
|
+
{ label: "Metrics & Telemetry", value: "metrics" },
|
|
56
|
+
{ label: "Auto-backup to Cloud", value: "backup" },
|
|
57
|
+
{ label: "Email Notifications", value: "email" },
|
|
58
|
+
],
|
|
59
|
+
defaultValue: ["auth", "metrics"],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: "FilePicker",
|
|
63
|
+
id: "video_file",
|
|
64
|
+
label: "Video File (.mp4)",
|
|
65
|
+
placeholder: "Choose an MP4 video file or enter path...",
|
|
66
|
+
pickerType: "file",
|
|
67
|
+
extensions: ["mp4"],
|
|
68
|
+
required: true,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "FilePicker",
|
|
72
|
+
id: "output_dir",
|
|
73
|
+
label: "Output Directory",
|
|
74
|
+
placeholder: "Choose output folder...",
|
|
75
|
+
pickerType: "folder",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: "Toggle",
|
|
79
|
+
id: "notify_team",
|
|
80
|
+
label: "Send notification to team on completion",
|
|
81
|
+
defaultValue: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "Button",
|
|
85
|
+
label: "Submit Configuration",
|
|
86
|
+
variant: "primary",
|
|
87
|
+
action: "submit",
|
|
88
|
+
message: "Proceed with the submitted configuration.",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
null,
|
|
93
|
+
2,
|
|
94
|
+
),
|
|
95
|
+
"```",
|
|
96
|
+
].join("\n");
|