@getpaseo/cli 0.3.0-beta.2 → 0.3.0-beta.3
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/dist/commands/hub/client.d.ts +19 -0
- package/dist/commands/hub/client.js +114 -0
- package/dist/commands/hub/deploy-input.d.ts +17 -0
- package/dist/commands/hub/deploy-input.js +264 -0
- package/dist/commands/hub/deploy.d.ts +17 -0
- package/dist/commands/hub/deploy.js +71 -0
- package/dist/commands/hub/error.d.ts +6 -0
- package/dist/commands/hub/error.js +9 -0
- package/dist/commands/hub/index.js +3 -1
- package/package.json +4 -4
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { HubDeployPartial } from "./deploy-input.js";
|
|
3
|
+
declare const installResponseSchema: z.ZodObject<{
|
|
4
|
+
projectSlug: z.ZodString;
|
|
5
|
+
version: z.ZodNumber;
|
|
6
|
+
versionId: z.ZodString;
|
|
7
|
+
active: z.ZodBoolean;
|
|
8
|
+
}, z.core.$strict>;
|
|
9
|
+
export type HubInstallResult = z.infer<typeof installResponseSchema>;
|
|
10
|
+
interface InstallHubConfigurationInput {
|
|
11
|
+
origin: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
projectSlug: string;
|
|
14
|
+
yaml: string;
|
|
15
|
+
partials?: readonly HubDeployPartial[];
|
|
16
|
+
}
|
|
17
|
+
export declare function installHubConfiguration(input: InstallHubConfigurationInput): Promise<HubInstallResult>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { HubDeployError } from "./error.js";
|
|
3
|
+
const installResponseSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
projectSlug: z.string().min(1),
|
|
6
|
+
version: z.number().int().positive(),
|
|
7
|
+
versionId: z.string().uuid(),
|
|
8
|
+
active: z.boolean(),
|
|
9
|
+
})
|
|
10
|
+
.strict();
|
|
11
|
+
const issuePathSchema = z.union([z.string(), z.array(z.union([z.string(), z.number()]))]);
|
|
12
|
+
const fieldIssueSchema = z.object({
|
|
13
|
+
field: z.string().optional(),
|
|
14
|
+
path: issuePathSchema.optional(),
|
|
15
|
+
message: z.string(),
|
|
16
|
+
});
|
|
17
|
+
const problemSchema = z.object({
|
|
18
|
+
type: z.string().optional(),
|
|
19
|
+
title: z.string().optional(),
|
|
20
|
+
status: z.number().int().optional(),
|
|
21
|
+
detail: z.string().optional(),
|
|
22
|
+
instance: z.string().optional(),
|
|
23
|
+
errors: z
|
|
24
|
+
.union([z.array(fieldIssueSchema), z.record(z.string(), z.array(z.string()))])
|
|
25
|
+
.optional(),
|
|
26
|
+
issues: z.array(fieldIssueSchema).optional(),
|
|
27
|
+
});
|
|
28
|
+
export async function installHubConfiguration(input) {
|
|
29
|
+
let response;
|
|
30
|
+
try {
|
|
31
|
+
response = await fetch(`${input.origin}/api/v1/configurations/install`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
35
|
+
"content-type": "application/json",
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
projectSlug: input.projectSlug,
|
|
39
|
+
yaml: input.yaml,
|
|
40
|
+
...(input.partials === undefined || input.partials.length === 0
|
|
41
|
+
? {}
|
|
42
|
+
: { partials: input.partials }),
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new HubDeployError("HUB_NETWORK_ERROR", `Could not reach Paseo Hub at ${input.origin}. Check the Hub URL and network connection.`);
|
|
48
|
+
}
|
|
49
|
+
if (response.status !== 201) {
|
|
50
|
+
throw await deploymentFailure(response, input.apiKey);
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return installResponseSchema.parse(await response.json());
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new HubDeployError("HUB_INVALID_RESPONSE", "Hub returned a malformed deployment response.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function deploymentFailure(response, apiKey) {
|
|
60
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
61
|
+
if (!contentType.toLowerCase().includes("application/problem+json")) {
|
|
62
|
+
return new HubDeployError("HUB_REQUEST_FAILED", `Hub deployment failed with HTTP ${response.status}.`);
|
|
63
|
+
}
|
|
64
|
+
let body;
|
|
65
|
+
try {
|
|
66
|
+
body = await response.json();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return new HubDeployError("HUB_INVALID_RESPONSE", `Hub returned malformed problem details for HTTP ${response.status}.`);
|
|
70
|
+
}
|
|
71
|
+
const parsed = problemSchema.safeParse(body);
|
|
72
|
+
if (!parsed.success ||
|
|
73
|
+
(parsed.data.status !== undefined && parsed.data.status !== response.status)) {
|
|
74
|
+
return new HubDeployError("HUB_INVALID_RESPONSE", `Hub returned nonconforming problem details for HTTP ${response.status}.`);
|
|
75
|
+
}
|
|
76
|
+
const title = parsed.data.title ?? `Hub deployment failed with HTTP ${response.status}`;
|
|
77
|
+
const detail = parsed.data.detail;
|
|
78
|
+
const message = detail === undefined ? title : `${title}: ${detail}`;
|
|
79
|
+
const details = formatFieldIssues(parsed.data.errors, parsed.data.issues);
|
|
80
|
+
const code = response.status === 422 ? "HUB_VALIDATION_FAILED" : "HUB_REQUEST_FAILED";
|
|
81
|
+
return new HubDeployError(code, redactSecret(message, apiKey), details === undefined ? undefined : redactSecret(details, apiKey));
|
|
82
|
+
}
|
|
83
|
+
function formatFieldIssues(errors, issues) {
|
|
84
|
+
const fieldIssues = Array.isArray(errors) ? errors : issues;
|
|
85
|
+
if (fieldIssues !== undefined) {
|
|
86
|
+
const lines = fieldIssues.map((issue) => {
|
|
87
|
+
const field = issue.field ?? formatIssuePath(issue.path);
|
|
88
|
+
return field === undefined ? issue.message : `${field}: ${issue.message}`;
|
|
89
|
+
});
|
|
90
|
+
return lines.length === 0 ? undefined : lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
if (errors === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
const lines = Object.entries(errors).flatMap(([field, messages]) => messages.map((message) => `${field}: ${message}`));
|
|
95
|
+
return lines.length === 0 ? undefined : lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
function formatIssuePath(path) {
|
|
98
|
+
if (path === undefined || typeof path === "string")
|
|
99
|
+
return path;
|
|
100
|
+
let formatted = "";
|
|
101
|
+
for (const segment of path) {
|
|
102
|
+
if (typeof segment === "number") {
|
|
103
|
+
formatted += `[${segment}]`;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
formatted += formatted.length === 0 ? segment : `.${segment}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return formatted || undefined;
|
|
110
|
+
}
|
|
111
|
+
function redactSecret(value, secret) {
|
|
112
|
+
return value.split(secret).join("[redacted]");
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface HubDeployPartial {
|
|
2
|
+
path: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HubDeployInput {
|
|
6
|
+
projectSlug: string;
|
|
7
|
+
yaml: string;
|
|
8
|
+
partials?: readonly HubDeployPartial[];
|
|
9
|
+
}
|
|
10
|
+
interface ResolveHubDeployInput {
|
|
11
|
+
cwd: string;
|
|
12
|
+
file?: string;
|
|
13
|
+
project?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function resolveHubDeployInput(input: ResolveHubDeployInput): Promise<HubDeployInput>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=deploy-input.d.ts.map
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { HubDeployError } from "./error.js";
|
|
5
|
+
const DEFAULT_CONFIGURATION_PATH = ".paseo/hub.yml";
|
|
6
|
+
const PROMPT_PARTIAL_ROOT = ".paseo/partials";
|
|
7
|
+
const PROMPT_PARTIAL_ROOT_PREFIX = `${PROMPT_PARTIAL_ROOT}/`;
|
|
8
|
+
const PROJECT_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
9
|
+
const MAX_CONFIGURATION_LENGTH = 1000000;
|
|
10
|
+
const MAX_PROMPT_PARTIAL_COUNT = 100;
|
|
11
|
+
const MAX_PROMPT_PARTIAL_PATH_LENGTH = 512;
|
|
12
|
+
const MAX_PROMPT_PARTIAL_CONTENT_BYTES = 1000000;
|
|
13
|
+
const MAX_PROMPT_PARTIAL_BUNDLE_BYTES = 5000000;
|
|
14
|
+
export async function resolveHubDeployInput(input) {
|
|
15
|
+
const file = input.file ?? DEFAULT_CONFIGURATION_PATH;
|
|
16
|
+
const projectRoot = path.resolve(input.cwd);
|
|
17
|
+
const configurationPath = resolveConfigurationPath(projectRoot, file);
|
|
18
|
+
const yaml = await readConfiguration(projectRoot, configurationPath, file);
|
|
19
|
+
const configuration = parseConfiguration(yaml);
|
|
20
|
+
const projectSlug = input.project ?? projectFromConfiguration(configuration);
|
|
21
|
+
if (projectSlug === undefined) {
|
|
22
|
+
throw new HubDeployError("HUB_PROJECT_REQUIRED", "Project is required. Pass --project <slug> or add top-level project to the YAML.");
|
|
23
|
+
}
|
|
24
|
+
if (!PROJECT_SLUG_PATTERN.test(projectSlug)) {
|
|
25
|
+
throw new HubDeployError("HUB_INVALID_PROJECT", "Project must be a bare slug such as my-project.");
|
|
26
|
+
}
|
|
27
|
+
const partials = await resolvePromptPartials(projectRoot, configuration);
|
|
28
|
+
return {
|
|
29
|
+
projectSlug,
|
|
30
|
+
yaml,
|
|
31
|
+
...(partials.length === 0 ? {} : { partials }),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function resolveConfigurationPath(cwd, file) {
|
|
35
|
+
if (file.length === 0 || file.includes("\0")) {
|
|
36
|
+
throw invalidConfigurationPath();
|
|
37
|
+
}
|
|
38
|
+
const segments = file.replaceAll("\\", "/").split("/");
|
|
39
|
+
if (segments.some((segment) => segment === "..")) {
|
|
40
|
+
throw invalidConfigurationPath();
|
|
41
|
+
}
|
|
42
|
+
const resolved = path.resolve(cwd, file);
|
|
43
|
+
const relative = path.relative(cwd, resolved);
|
|
44
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
45
|
+
throw invalidConfigurationPath();
|
|
46
|
+
}
|
|
47
|
+
return resolved;
|
|
48
|
+
}
|
|
49
|
+
async function readConfiguration(projectRoot, configurationPath, displayPath) {
|
|
50
|
+
const unsafePath = () => new HubDeployError("HUB_CONFIGURATION_UNSAFE_PATH", `Hub configuration at ${displayPath} must not use a symlink.`);
|
|
51
|
+
await rejectSymlinkComponents(projectRoot, configurationPath, unsafePath);
|
|
52
|
+
let stats;
|
|
53
|
+
try {
|
|
54
|
+
stats = await lstat(configurationPath);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw configurationReadError(displayPath, error);
|
|
58
|
+
}
|
|
59
|
+
if (stats.isSymbolicLink()) {
|
|
60
|
+
throw unsafePath();
|
|
61
|
+
}
|
|
62
|
+
if (!stats.isFile()) {
|
|
63
|
+
throw new HubDeployError("HUB_CONFIGURATION_NOT_FILE", `Hub configuration at ${displayPath} must be a regular file.`);
|
|
64
|
+
}
|
|
65
|
+
if (!hasReadPermission(stats.mode)) {
|
|
66
|
+
throw new HubDeployError("HUB_CONFIGURATION_UNREADABLE", `Could not read Hub configuration at ${displayPath}. Check file permissions.`);
|
|
67
|
+
}
|
|
68
|
+
let bytes;
|
|
69
|
+
try {
|
|
70
|
+
bytes = await readFile(configurationPath);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw configurationReadError(displayPath, error);
|
|
74
|
+
}
|
|
75
|
+
const yaml = bytes.toString("utf8");
|
|
76
|
+
if (yaml.length > MAX_CONFIGURATION_LENGTH) {
|
|
77
|
+
throw new HubDeployError("HUB_CONFIGURATION_TOO_LARGE", `Hub configuration at ${displayPath} exceeds the ${MAX_CONFIGURATION_LENGTH}-character limit.`);
|
|
78
|
+
}
|
|
79
|
+
return yaml;
|
|
80
|
+
}
|
|
81
|
+
function parseConfiguration(yaml) {
|
|
82
|
+
let configuration;
|
|
83
|
+
try {
|
|
84
|
+
configuration = YAML.parse(yaml);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new HubDeployError("HUB_INVALID_CONFIGURATION", "Hub configuration is not valid YAML.");
|
|
88
|
+
}
|
|
89
|
+
if (!isRecord(configuration)) {
|
|
90
|
+
throw new HubDeployError("HUB_INVALID_CONFIGURATION", "Hub configuration must be a YAML mapping.");
|
|
91
|
+
}
|
|
92
|
+
return configuration;
|
|
93
|
+
}
|
|
94
|
+
function projectFromConfiguration(configuration) {
|
|
95
|
+
const project = configuration["project"];
|
|
96
|
+
if (project === undefined)
|
|
97
|
+
return undefined;
|
|
98
|
+
if (typeof project !== "string") {
|
|
99
|
+
throw new HubDeployError("HUB_INVALID_PROJECT", "Top-level project must be a bare project slug.");
|
|
100
|
+
}
|
|
101
|
+
return project;
|
|
102
|
+
}
|
|
103
|
+
async function resolvePromptPartials(projectRoot, configuration) {
|
|
104
|
+
const references = collectPromptPartialReferences(configuration);
|
|
105
|
+
if (references.length > MAX_PROMPT_PARTIAL_COUNT) {
|
|
106
|
+
throw new HubDeployError("HUB_PARTIAL_LIMIT_EXCEEDED", `Hub configuration references ${references.length} partials; the limit is ${MAX_PROMPT_PARTIAL_COUNT}.`);
|
|
107
|
+
}
|
|
108
|
+
const partials = [];
|
|
109
|
+
let bundleBytes = 0;
|
|
110
|
+
for (const reference of references) {
|
|
111
|
+
const partialPath = path.resolve(projectRoot, PROMPT_PARTIAL_ROOT, ...reference.path.split("/"));
|
|
112
|
+
const content = await readPartial(projectRoot, partialPath, reference.path);
|
|
113
|
+
const contentBytes = Buffer.byteLength(content, "utf8");
|
|
114
|
+
if (contentBytes > MAX_PROMPT_PARTIAL_CONTENT_BYTES) {
|
|
115
|
+
throw new HubDeployError("HUB_PARTIAL_TOO_LARGE", `Referenced Hub partial ${reference.path} exceeds the ${MAX_PROMPT_PARTIAL_CONTENT_BYTES}-byte limit.`);
|
|
116
|
+
}
|
|
117
|
+
bundleBytes += contentBytes;
|
|
118
|
+
if (bundleBytes > MAX_PROMPT_PARTIAL_BUNDLE_BYTES) {
|
|
119
|
+
throw new HubDeployError("HUB_PARTIAL_BUNDLE_TOO_LARGE", `Referenced Hub partials exceed the ${MAX_PROMPT_PARTIAL_BUNDLE_BYTES}-byte combined limit.`);
|
|
120
|
+
}
|
|
121
|
+
partials.push({ path: reference.path, content });
|
|
122
|
+
}
|
|
123
|
+
return partials;
|
|
124
|
+
}
|
|
125
|
+
function collectPromptPartialReferences(configuration) {
|
|
126
|
+
const references = [];
|
|
127
|
+
const seen = new Set();
|
|
128
|
+
const triggers = configuration["triggers"];
|
|
129
|
+
if (!Array.isArray(triggers))
|
|
130
|
+
return references;
|
|
131
|
+
for (const trigger of triggers) {
|
|
132
|
+
if (!isRecord(trigger) || !Array.isArray(trigger["steps"]))
|
|
133
|
+
continue;
|
|
134
|
+
for (const step of trigger["steps"]) {
|
|
135
|
+
if (!isRecord(step) || !Array.isArray(step["prompt"]))
|
|
136
|
+
continue;
|
|
137
|
+
for (const block of step["prompt"]) {
|
|
138
|
+
if (!isRecord(block) || !Object.hasOwn(block, "include"))
|
|
139
|
+
continue;
|
|
140
|
+
const include = block["include"];
|
|
141
|
+
if (typeof include !== "string") {
|
|
142
|
+
throw new HubDeployError("HUB_PARTIAL_PATH_INVALID", "Hub partial include path must be a string.");
|
|
143
|
+
}
|
|
144
|
+
const normalizedPath = normalizePromptPartialPath(include);
|
|
145
|
+
if (seen.has(normalizedPath)) {
|
|
146
|
+
throw new HubDeployError("HUB_PARTIAL_DUPLICATE", `Hub partial ${normalizedPath} is referenced more than once. Remove the duplicate include.`);
|
|
147
|
+
}
|
|
148
|
+
seen.add(normalizedPath);
|
|
149
|
+
references.push({ path: normalizedPath });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return references;
|
|
154
|
+
}
|
|
155
|
+
function normalizePromptPartialPath(value) {
|
|
156
|
+
const decoded = decodePromptPartialPath(value);
|
|
157
|
+
if (decoded.length === 0)
|
|
158
|
+
throw invalidPromptPartialPath(value);
|
|
159
|
+
if (/^(?:[a-zA-Z]:[\\/]|[\\/]{1,2})/u.test(decoded)) {
|
|
160
|
+
throw invalidPromptPartialPath(value);
|
|
161
|
+
}
|
|
162
|
+
const segments = decoded.replaceAll("\\", "/").split("/");
|
|
163
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) {
|
|
164
|
+
throw invalidPromptPartialPath(value);
|
|
165
|
+
}
|
|
166
|
+
const canonical = `${PROMPT_PARTIAL_ROOT}/${segments.join("/")}`;
|
|
167
|
+
if (!canonical.startsWith(PROMPT_PARTIAL_ROOT_PREFIX)) {
|
|
168
|
+
throw invalidPromptPartialPath(value);
|
|
169
|
+
}
|
|
170
|
+
if (canonical.length > MAX_PROMPT_PARTIAL_PATH_LENGTH) {
|
|
171
|
+
throw new HubDeployError("HUB_PARTIAL_PATH_TOO_LONG", `Hub partial path ${value} exceeds the ${MAX_PROMPT_PARTIAL_PATH_LENGTH}-character limit.`);
|
|
172
|
+
}
|
|
173
|
+
return canonical.slice(PROMPT_PARTIAL_ROOT_PREFIX.length);
|
|
174
|
+
}
|
|
175
|
+
function decodePromptPartialPath(value) {
|
|
176
|
+
let decoded = value;
|
|
177
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
178
|
+
let next;
|
|
179
|
+
try {
|
|
180
|
+
next = decodeURIComponent(decoded);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
throw invalidPromptPartialPath(value);
|
|
184
|
+
}
|
|
185
|
+
if (next === decoded)
|
|
186
|
+
break;
|
|
187
|
+
decoded = next;
|
|
188
|
+
}
|
|
189
|
+
if (decoded.includes("\0") || /%[0-9a-f]{2}/iu.test(decoded)) {
|
|
190
|
+
throw invalidPromptPartialPath(value);
|
|
191
|
+
}
|
|
192
|
+
return decoded;
|
|
193
|
+
}
|
|
194
|
+
async function readPartial(projectRoot, partialPath, displayPath) {
|
|
195
|
+
await rejectSymlinkComponents(projectRoot, partialPath, () => new HubDeployError("HUB_PARTIAL_UNSAFE_PATH", `Referenced Hub partial ${displayPath} must not use a symlink.`));
|
|
196
|
+
let stats;
|
|
197
|
+
try {
|
|
198
|
+
stats = await lstat(partialPath);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (errorCode(error) === "ENOENT") {
|
|
202
|
+
throw new HubDeployError("HUB_PARTIAL_MISSING", `Referenced Hub partial ${displayPath} does not exist.`);
|
|
203
|
+
}
|
|
204
|
+
throw new HubDeployError("HUB_PARTIAL_UNREADABLE", `Could not read referenced Hub partial ${displayPath}. Check the file and permissions.`);
|
|
205
|
+
}
|
|
206
|
+
if (stats.isSymbolicLink()) {
|
|
207
|
+
throw new HubDeployError("HUB_PARTIAL_UNSAFE_PATH", `Referenced Hub partial ${displayPath} must not be a symlink.`);
|
|
208
|
+
}
|
|
209
|
+
if (!stats.isFile()) {
|
|
210
|
+
throw new HubDeployError("HUB_PARTIAL_NOT_FILE", `Referenced Hub partial ${displayPath} must be a regular file.`);
|
|
211
|
+
}
|
|
212
|
+
if (!hasReadPermission(stats.mode)) {
|
|
213
|
+
throw new HubDeployError("HUB_PARTIAL_UNREADABLE", `Could not read referenced Hub partial ${displayPath}. Check the file and permissions.`);
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
return (await readFile(partialPath)).toString("utf8");
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
throw new HubDeployError("HUB_PARTIAL_UNREADABLE", `Could not read referenced Hub partial ${displayPath}. Check the file and permissions.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function rejectSymlinkComponents(root, target, error) {
|
|
223
|
+
const relative = path.relative(root, target);
|
|
224
|
+
let current = root;
|
|
225
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
226
|
+
current = path.join(current, segment);
|
|
227
|
+
try {
|
|
228
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
229
|
+
throw error();
|
|
230
|
+
}
|
|
231
|
+
catch (failure) {
|
|
232
|
+
if (failure instanceof HubDeployError)
|
|
233
|
+
throw failure;
|
|
234
|
+
if (errorCode(failure) === "ENOENT")
|
|
235
|
+
return;
|
|
236
|
+
throw error();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function configurationReadError(displayPath, error) {
|
|
241
|
+
if (errorCode(error) === "ENOENT") {
|
|
242
|
+
return new HubDeployError("HUB_CONFIGURATION_UNREADABLE", `Could not read Hub configuration at ${displayPath}. Pass an existing YAML file.`);
|
|
243
|
+
}
|
|
244
|
+
return new HubDeployError("HUB_CONFIGURATION_UNREADABLE", `Could not read Hub configuration at ${displayPath}. Check the file and permissions.`);
|
|
245
|
+
}
|
|
246
|
+
function invalidConfigurationPath() {
|
|
247
|
+
return new HubDeployError("HUB_CONFIGURATION_PATH_INVALID", "Hub configuration path must stay within the current project root; parent-directory paths are not allowed.");
|
|
248
|
+
}
|
|
249
|
+
function invalidPromptPartialPath(value) {
|
|
250
|
+
return new HubDeployError("HUB_PARTIAL_PATH_INVALID", `Hub partial path must be a safe relative path under .paseo/partials/: ${value}`);
|
|
251
|
+
}
|
|
252
|
+
function hasReadPermission(mode) {
|
|
253
|
+
return (mode & 0o444) !== 0;
|
|
254
|
+
}
|
|
255
|
+
function errorCode(error) {
|
|
256
|
+
if (typeof error !== "object" || error === null)
|
|
257
|
+
return undefined;
|
|
258
|
+
const code = Reflect.get(error, "code");
|
|
259
|
+
return typeof code === "string" ? code : undefined;
|
|
260
|
+
}
|
|
261
|
+
function isRecord(value) {
|
|
262
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=deploy-input.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { type SingleResult } from "../../output/index.js";
|
|
3
|
+
import { type HubInstallResult } from "./client.js";
|
|
4
|
+
export interface HubDeployOptions {
|
|
5
|
+
file?: string;
|
|
6
|
+
project?: string;
|
|
7
|
+
hub?: string;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
}
|
|
10
|
+
interface HubDeployEnvironment {
|
|
11
|
+
cwd: string;
|
|
12
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
}
|
|
14
|
+
export declare function runHubDeploy(options: HubDeployOptions, environment?: HubDeployEnvironment): Promise<SingleResult<HubInstallResult>>;
|
|
15
|
+
export declare function addHubDeployCommand(hub: Command): void;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=deploy.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { withOutput } from "../../output/index.js";
|
|
2
|
+
import { addJsonOption } from "../../utils/command-options.js";
|
|
3
|
+
import { installHubConfiguration } from "./client.js";
|
|
4
|
+
import { resolveHubDeployInput } from "./deploy-input.js";
|
|
5
|
+
import { HubDeployError } from "./error.js";
|
|
6
|
+
const resultSchema = {
|
|
7
|
+
idField: "versionId",
|
|
8
|
+
columns: [
|
|
9
|
+
{ header: "PROJECT", field: "projectSlug" },
|
|
10
|
+
{ header: "VERSION", field: "version" },
|
|
11
|
+
{ header: "VERSION ID", field: "versionId" },
|
|
12
|
+
{ header: "ACTIVE", field: "active" },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
export async function runHubDeploy(options, environment = { cwd: process.cwd(), env: process.env }) {
|
|
16
|
+
const origin = options.hub ?? environment.env.PASEO_HUB_URL;
|
|
17
|
+
const apiKey = options.apiKey ?? environment.env.PASEO_HUB_API_KEY;
|
|
18
|
+
if (!origin) {
|
|
19
|
+
throw new HubDeployError("HUB_ORIGIN_REQUIRED", "Hub origin is required. Pass --hub <origin> or set PASEO_HUB_URL.");
|
|
20
|
+
}
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
throw new HubDeployError("HUB_API_KEY_REQUIRED", "Hub API key is required. Pass --api-key <secret> or set PASEO_HUB_API_KEY.");
|
|
23
|
+
}
|
|
24
|
+
const normalizedOrigin = parseHubOrigin(origin);
|
|
25
|
+
const deployInput = await resolveHubDeployInput({
|
|
26
|
+
cwd: environment.cwd,
|
|
27
|
+
...(options.file === undefined ? {} : { file: options.file }),
|
|
28
|
+
...(options.project === undefined ? {} : { project: options.project }),
|
|
29
|
+
});
|
|
30
|
+
const deployed = await installHubConfiguration({
|
|
31
|
+
origin: normalizedOrigin,
|
|
32
|
+
apiKey,
|
|
33
|
+
...deployInput,
|
|
34
|
+
});
|
|
35
|
+
return { type: "single", data: deployed, schema: resultSchema };
|
|
36
|
+
}
|
|
37
|
+
export function addHubDeployCommand(hub) {
|
|
38
|
+
addJsonOption(hub
|
|
39
|
+
.command("deploy")
|
|
40
|
+
.description("Install and activate a Hub configuration")
|
|
41
|
+
.argument("[file]", "Hub configuration YAML", ".paseo/hub.yml")
|
|
42
|
+
.option("-p, --project <slug>", "Target project slug")
|
|
43
|
+
.option("--hub <origin>", "Paseo Hub origin")
|
|
44
|
+
.option("--api-key <secret>", "Organization API key")).action(withOutput(async (...args) => {
|
|
45
|
+
const file = args[0];
|
|
46
|
+
const options = args.at(-2);
|
|
47
|
+
return runHubDeploy({ ...options, file });
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
function parseHubOrigin(value) {
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(value);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw invalidHubOrigin();
|
|
57
|
+
}
|
|
58
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
59
|
+
url.username ||
|
|
60
|
+
url.password ||
|
|
61
|
+
url.pathname !== "/" ||
|
|
62
|
+
url.search ||
|
|
63
|
+
url.hash) {
|
|
64
|
+
throw invalidHubOrigin();
|
|
65
|
+
}
|
|
66
|
+
return url.origin;
|
|
67
|
+
}
|
|
68
|
+
function invalidHubOrigin() {
|
|
69
|
+
return new HubDeployError("HUB_INVALID_ORIGIN", "Hub URL must be an HTTP or HTTPS origin without credentials, path, query, or hash.");
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -4,6 +4,7 @@ import { withOutput } from "../../output/index.js";
|
|
|
4
4
|
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
5
5
|
import { connectToDaemon } from "../../utils/client.js";
|
|
6
6
|
import { createDeviceAuthorizationWorkflow } from "./device-authorization.js";
|
|
7
|
+
import { addHubDeployCommand } from "./deploy.js";
|
|
7
8
|
const productionEnvironment = {
|
|
8
9
|
connect: (host) => connectToDaemon({ host }),
|
|
9
10
|
authorize: (url, displayName) => createDeviceAuthorizationWorkflow().authorize(url, displayName),
|
|
@@ -48,7 +49,7 @@ async function withClient(environment, host, action) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
export function createHubCommand(environment = productionEnvironment) {
|
|
51
|
-
const hub = new Command("hub").description("Manage
|
|
52
|
+
const hub = new Command("hub").description("Manage Paseo Hub");
|
|
52
53
|
addJsonAndDaemonHostOptions(hub.command("connect").argument("<url>").option("--token <token>")).action(withOutput(async (...args) => {
|
|
53
54
|
const url = args[0];
|
|
54
55
|
const options = args.at(-2);
|
|
@@ -77,6 +78,7 @@ export function createHubCommand(environment = productionEnvironment) {
|
|
|
77
78
|
return result(response.status, response.warning);
|
|
78
79
|
});
|
|
79
80
|
}));
|
|
81
|
+
addHubDeployCommand(hub);
|
|
80
82
|
return hub;
|
|
81
83
|
}
|
|
82
84
|
function suggestedDisplayName(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpaseo/cli",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.3",
|
|
4
4
|
"description": "Paseo CLI - control your AI coding agents from the command line",
|
|
5
5
|
"bin": {
|
|
6
6
|
"paseo": "bin/paseo"
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@clack/prompts": "^1.0.0",
|
|
31
|
-
"@getpaseo/client": "0.3.0-beta.
|
|
32
|
-
"@getpaseo/protocol": "0.3.0-beta.
|
|
33
|
-
"@getpaseo/server": "0.3.0-beta.
|
|
31
|
+
"@getpaseo/client": "0.3.0-beta.3",
|
|
32
|
+
"@getpaseo/protocol": "0.3.0-beta.3",
|
|
33
|
+
"@getpaseo/server": "0.3.0-beta.3",
|
|
34
34
|
"chalk": "^5.3.0",
|
|
35
35
|
"commander": "^12.0.0",
|
|
36
36
|
"mime-types": "^2.1.35",
|