@getpaseo/cli 0.3.0 → 0.4.0-beta.1
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/cli.js +0 -5
- package/dist/commands/hub/authority.d.ts +17 -0
- package/dist/commands/hub/authority.js +18 -0
- package/dist/commands/hub/connect.d.ts +21 -0
- package/dist/commands/hub/connect.js +34 -0
- package/dist/commands/hub/credentials.d.ts +21 -0
- package/dist/commands/hub/credentials.js +143 -0
- package/dist/commands/hub/daemon-client.d.ts +27 -0
- package/dist/commands/hub/daemon-client.js +14 -0
- package/dist/commands/hub/deploy-bundle.d.ts +16 -0
- package/dist/commands/hub/deploy-bundle.js +248 -0
- package/dist/commands/hub/deploy.d.ts +25 -4
- package/dist/commands/hub/deploy.js +64 -48
- package/dist/commands/hub/disconnect.d.ts +16 -0
- package/dist/commands/hub/disconnect.js +24 -0
- package/dist/commands/hub/error.d.ts +1 -1
- package/dist/commands/hub/error.js +2 -2
- package/dist/commands/hub/help.d.ts +3 -0
- package/dist/commands/hub/help.js +5 -0
- package/dist/commands/hub/hub-client/index.d.ts +18 -0
- package/dist/commands/hub/hub-client/index.js +88 -0
- package/dist/commands/hub/hub-client/internal/contracts.d.ts +65 -0
- package/dist/commands/hub/hub-client/internal/contracts.js +47 -0
- package/dist/commands/hub/hub-client/internal/problem.d.ts +3 -0
- package/dist/commands/hub/hub-client/internal/problem.js +72 -0
- package/dist/commands/hub/hub-client/internal/transport.d.ts +15 -0
- package/dist/commands/hub/hub-client/internal/transport.js +30 -0
- package/dist/commands/hub/index.d.ts +15 -25
- package/dist/commands/hub/index.js +64 -74
- package/dist/commands/hub/{device-authorization.d.ts → login-flow.d.ts} +12 -15
- package/dist/commands/hub/login-flow.js +78 -0
- package/dist/commands/hub/login.d.ts +21 -0
- package/dist/commands/hub/login.js +34 -0
- package/dist/commands/hub/logout.d.ts +31 -0
- package/dist/commands/hub/logout.js +65 -0
- package/dist/commands/hub/origin.d.ts +2 -0
- package/dist/commands/hub/origin.js +27 -0
- package/dist/commands/hub/projects.d.ts +24 -0
- package/dist/commands/hub/projects.js +49 -0
- package/dist/commands/hub/reporter.d.ts +10 -0
- package/dist/commands/hub/reporter.js +11 -0
- package/dist/commands/hub/status-output.d.ts +13 -0
- package/dist/commands/hub/status-output.js +30 -0
- package/dist/commands/provider/diagnostic.d.ts +12 -0
- package/dist/commands/provider/diagnostic.js +31 -0
- package/dist/commands/provider/index.js +5 -0
- package/dist/commands/schedule/types.d.ts +1 -4
- package/dist/commands/workspace/index.js +10 -0
- package/dist/commands/workspace/rename.d.ts +22 -0
- package/dist/commands/workspace/rename.js +61 -0
- package/dist/output/types.d.ts +1 -1
- package/package.json +4 -4
- package/dist/commands/chat/create.d.ts +0 -9
- package/dist/commands/chat/create.js +0 -23
- package/dist/commands/chat/delete.d.ts +0 -6
- package/dist/commands/chat/delete.js +0 -20
- package/dist/commands/chat/index.d.ts +0 -3
- package/dist/commands/chat/index.js +0 -47
- package/dist/commands/chat/inspect.d.ts +0 -6
- package/dist/commands/chat/inspect.js +0 -20
- package/dist/commands/chat/ls.d.ts +0 -6
- package/dist/commands/chat/ls.js +0 -20
- package/dist/commands/chat/post.d.ts +0 -9
- package/dist/commands/chat/post.js +0 -28
- package/dist/commands/chat/read.d.ts +0 -11
- package/dist/commands/chat/read.js +0 -40
- package/dist/commands/chat/schema.d.ts +0 -36
- package/dist/commands/chat/schema.js +0 -81
- package/dist/commands/chat/shared.d.ts +0 -19
- package/dist/commands/chat/shared.js +0 -118
- package/dist/commands/chat/wait.d.ts +0 -9
- package/dist/commands/chat/wait.js +0 -45
- package/dist/commands/hub/client.d.ts +0 -19
- package/dist/commands/hub/client.js +0 -114
- package/dist/commands/hub/cloud-device-authorization.d.ts +0 -45
- package/dist/commands/hub/cloud-device-authorization.js +0 -92
- package/dist/commands/hub/deploy-input.d.ts +0 -17
- package/dist/commands/hub/deploy-input.js +0 -264
- package/dist/commands/hub/device-authorization.js +0 -87
- package/dist/commands/loop/index.d.ts +0 -3
- package/dist/commands/loop/index.js +0 -18
- package/dist/commands/loop/inspect.d.ts +0 -13
- package/dist/commands/loop/inspect.js +0 -100
- package/dist/commands/loop/logs.d.ts +0 -8
- package/dist/commands/loop/logs.js +0 -69
- package/dist/commands/loop/ls.d.ts +0 -18
- package/dist/commands/loop/ls.js +0 -62
- package/dist/commands/loop/run.d.ts +0 -30
- package/dist/commands/loop/run.js +0 -162
- package/dist/commands/loop/stop.d.ts +0 -15
- package/dist/commands/loop/stop.js +0 -56
- package/dist/commands/loop/types.d.ts +0 -138
- package/dist/commands/loop/types.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Command, Option } from "commander";
|
|
2
2
|
import { createAgentCommand } from "./commands/agent/index.js";
|
|
3
3
|
import { createDaemonCommand } from "./commands/daemon/index.js";
|
|
4
|
-
import { createChatCommand } from "./commands/chat/index.js";
|
|
5
|
-
import { createLoopCommand } from "./commands/loop/index.js";
|
|
6
4
|
import { createPermitCommand } from "./commands/permit/index.js";
|
|
7
5
|
import { createProviderCommand } from "./commands/provider/index.js";
|
|
8
6
|
import { createScheduleCommand } from "./commands/schedule/index.js";
|
|
@@ -107,13 +105,10 @@ export function createCli() {
|
|
|
107
105
|
program.addCommand(createDaemonCommand());
|
|
108
106
|
program.addCommand(createHubCommand());
|
|
109
107
|
// Chat commands
|
|
110
|
-
program.addCommand(createChatCommand());
|
|
111
108
|
// Terminal commands
|
|
112
109
|
program.addCommand(createTerminalCommand());
|
|
113
110
|
// Workspace script commands
|
|
114
111
|
program.addCommand(createScriptCommand());
|
|
115
|
-
// Loop commands
|
|
116
|
-
program.addCommand(createLoopCommand());
|
|
117
112
|
// Schedule commands
|
|
118
113
|
program.addCommand(createScheduleCommand());
|
|
119
114
|
program.addCommand(createHeartbeatCommand());
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HubCredentialStore } from "./credentials.js";
|
|
2
|
+
export interface HubAuthorityOptions {
|
|
3
|
+
origin?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
interface ResolveHubInput {
|
|
7
|
+
options: HubAuthorityOptions;
|
|
8
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
9
|
+
credentials: HubCredentialStore;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_HUB_ORIGIN = "https://hub.paseo.sh";
|
|
12
|
+
export declare function resolveHubOrigin(input: ResolveHubInput): string;
|
|
13
|
+
export declare function resolveHubCredential(input: ResolveHubInput & {
|
|
14
|
+
origin: string;
|
|
15
|
+
}): string;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=authority.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { HubCommandError } from "./error.js";
|
|
2
|
+
import { normalizeHubOrigin } from "./origin.js";
|
|
3
|
+
export const DEFAULT_HUB_ORIGIN = "https://hub.paseo.sh";
|
|
4
|
+
export function resolveHubOrigin(input) {
|
|
5
|
+
const configuredOrigin = input.options.origin ?? input.env.PASEO_HUB_URL;
|
|
6
|
+
const selectedOrigin = configuredOrigin ?? input.credentials.active()?.origin ?? DEFAULT_HUB_ORIGIN;
|
|
7
|
+
return normalizeHubOrigin(selectedOrigin);
|
|
8
|
+
}
|
|
9
|
+
export function resolveHubCredential(input) {
|
|
10
|
+
const explicitCredential = input.options.apiKey ?? input.env.PASEO_HUB_API_KEY;
|
|
11
|
+
if (explicitCredential !== undefined)
|
|
12
|
+
return explicitCredential;
|
|
13
|
+
const stored = input.credentials.get(input.origin);
|
|
14
|
+
if (stored !== null)
|
|
15
|
+
return stored.credential;
|
|
16
|
+
throw new HubCommandError("HUB_API_KEY_REQUIRED", `No stored Hub login matches ${input.origin}. Run \`paseo hub login ${input.origin}\`, pass --api-key <secret>, or set PASEO_HUB_API_KEY.`);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=authority.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { HubHttpClient } from "./hub-client/index.js";
|
|
3
|
+
import type { HubCredentialStore } from "./credentials.js";
|
|
4
|
+
import type { HubDaemonConnection } from "./daemon-client.js";
|
|
5
|
+
import { type HubReporter } from "./reporter.js";
|
|
6
|
+
interface HubConnectOptions {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
host?: string;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface HubConnectDependencies {
|
|
12
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
credentials: HubCredentialStore;
|
|
14
|
+
hub: Pick<HubHttpClient, "issueEnrollmentToken">;
|
|
15
|
+
daemon: HubDaemonConnection;
|
|
16
|
+
reporter: HubReporter;
|
|
17
|
+
}
|
|
18
|
+
export declare function runHubConnect(originInput: string | undefined, options: HubConnectOptions, dependencies: HubConnectDependencies): Promise<import("../../output/types.js").ListResult<import("./status-output.js").HubRow>>;
|
|
19
|
+
export declare function addHubConnectCommand(parent: Command, dependencies: HubConnectDependencies): void;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { withOutput } from "../../output/index.js";
|
|
2
|
+
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
3
|
+
import { resolveHubCredential, resolveHubOrigin } from "./authority.js";
|
|
4
|
+
import { withHubDaemon } from "./daemon-client.js";
|
|
5
|
+
import { hubStatusResult } from "./status-output.js";
|
|
6
|
+
import { reportHubProgress } from "./reporter.js";
|
|
7
|
+
import { addHubResolutionHelp } from "./help.js";
|
|
8
|
+
export async function runHubConnect(originInput, options, dependencies) {
|
|
9
|
+
const resolution = {
|
|
10
|
+
options: { origin: originInput, apiKey: options.apiKey },
|
|
11
|
+
env: dependencies.env,
|
|
12
|
+
credentials: dependencies.credentials,
|
|
13
|
+
};
|
|
14
|
+
const origin = resolveHubOrigin(resolution);
|
|
15
|
+
reportHubProgress(dependencies.reporter, options, `Connecting this daemon to ${origin}`);
|
|
16
|
+
const credential = resolveHubCredential({ ...resolution, origin });
|
|
17
|
+
const token = await dependencies.hub.issueEnrollmentToken(origin, credential);
|
|
18
|
+
return withHubDaemon(dependencies.daemon, options.host, async (daemon) => {
|
|
19
|
+
const response = await daemon.connectHub(origin, token);
|
|
20
|
+
return hubStatusResult(response.status);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function addHubConnectCommand(parent, dependencies) {
|
|
24
|
+
addJsonAndDaemonHostOptions(addHubResolutionHelp(parent
|
|
25
|
+
.command("connect")
|
|
26
|
+
.description("Enroll this daemon with a Paseo Hub")
|
|
27
|
+
.argument("[origin]", "Paseo Hub origin")
|
|
28
|
+
.option("--api-key <secret>", "Organization API key"))).action(withOutput(async (...args) => {
|
|
29
|
+
const origin = args[0];
|
|
30
|
+
const options = args.at(-2);
|
|
31
|
+
return runHubConnect(origin, options, dependencies);
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface StoredHubCredential {
|
|
2
|
+
origin: string;
|
|
3
|
+
credential: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HubCredentialStore {
|
|
6
|
+
active(): StoredHubCredential | null;
|
|
7
|
+
get(origin: string): StoredHubCredential | null;
|
|
8
|
+
save(credential: StoredHubCredential): void;
|
|
9
|
+
logoutActive(): StoredHubCredential | null;
|
|
10
|
+
}
|
|
11
|
+
export declare class PrivateHubCredentialStore implements HubCredentialStore {
|
|
12
|
+
private readonly filePath;
|
|
13
|
+
constructor(env?: Readonly<Record<string, string | undefined>>);
|
|
14
|
+
active(): StoredHubCredential | null;
|
|
15
|
+
get(origin: string): StoredHubCredential | null;
|
|
16
|
+
save(credential: StoredHubCredential): void;
|
|
17
|
+
logoutActive(): StoredHubCredential | null;
|
|
18
|
+
private read;
|
|
19
|
+
private write;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { HubCommandError } from "./error.js";
|
|
7
|
+
import { normalizeHubOrigin } from "./origin.js";
|
|
8
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
9
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
10
|
+
const credentialRecordSchema = z
|
|
11
|
+
.object({ origin: z.string(), credential: z.string().min(1) })
|
|
12
|
+
.strict();
|
|
13
|
+
const credentialFileSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
version: z.literal(1),
|
|
16
|
+
activeOrigin: z.string().nullable(),
|
|
17
|
+
credentials: z.array(credentialRecordSchema),
|
|
18
|
+
})
|
|
19
|
+
.strict()
|
|
20
|
+
.superRefine((value, context) => {
|
|
21
|
+
const origins = new Set();
|
|
22
|
+
for (const record of value.credentials) {
|
|
23
|
+
let normalized;
|
|
24
|
+
try {
|
|
25
|
+
normalized = normalizeHubOrigin(record.origin);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
context.addIssue({ code: "custom", message: "Credential origin is invalid" });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (normalized !== record.origin || origins.has(record.origin)) {
|
|
32
|
+
context.addIssue({
|
|
33
|
+
code: "custom",
|
|
34
|
+
message: "Credential origins must be unique and normalized",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
origins.add(record.origin);
|
|
38
|
+
}
|
|
39
|
+
if (value.activeOrigin !== null && !origins.has(value.activeOrigin)) {
|
|
40
|
+
context.addIssue({ code: "custom", message: "Active credential is missing" });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
export class PrivateHubCredentialStore {
|
|
44
|
+
constructor(env = process.env) {
|
|
45
|
+
this.filePath = path.join(resolvePaseoHome(env), "hub-credentials.json");
|
|
46
|
+
}
|
|
47
|
+
active() {
|
|
48
|
+
const data = this.read();
|
|
49
|
+
if (data.activeOrigin === null)
|
|
50
|
+
return null;
|
|
51
|
+
return data.credentials.find((record) => record.origin === data.activeOrigin) ?? null;
|
|
52
|
+
}
|
|
53
|
+
get(origin) {
|
|
54
|
+
const normalizedOrigin = normalizeHubOrigin(origin);
|
|
55
|
+
return this.read().credentials.find((record) => record.origin === normalizedOrigin) ?? null;
|
|
56
|
+
}
|
|
57
|
+
save(credential) {
|
|
58
|
+
const origin = normalizeHubOrigin(credential.origin);
|
|
59
|
+
const current = this.read();
|
|
60
|
+
const remaining = current.credentials.filter((record) => record.origin !== origin);
|
|
61
|
+
this.write({
|
|
62
|
+
version: 1,
|
|
63
|
+
activeOrigin: origin,
|
|
64
|
+
credentials: [...remaining, { origin, credential: credential.credential }],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
logoutActive() {
|
|
68
|
+
const current = this.read();
|
|
69
|
+
if (current.activeOrigin === null)
|
|
70
|
+
return null;
|
|
71
|
+
const removed = current.credentials.find((record) => record.origin === current.activeOrigin);
|
|
72
|
+
if (removed === undefined)
|
|
73
|
+
throw invalidCredentialFile();
|
|
74
|
+
this.write({
|
|
75
|
+
version: 1,
|
|
76
|
+
activeOrigin: null,
|
|
77
|
+
credentials: current.credentials.filter((record) => record.origin !== current.activeOrigin),
|
|
78
|
+
});
|
|
79
|
+
return removed;
|
|
80
|
+
}
|
|
81
|
+
read() {
|
|
82
|
+
let contents;
|
|
83
|
+
try {
|
|
84
|
+
contents = readFileSync(this.filePath, "utf8");
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isMissingFile(error))
|
|
88
|
+
return { version: 1, activeOrigin: null, credentials: [] };
|
|
89
|
+
throw credentialStorageError();
|
|
90
|
+
}
|
|
91
|
+
chmodPrivate(this.filePath, PRIVATE_FILE_MODE);
|
|
92
|
+
try {
|
|
93
|
+
return credentialFileSchema.parse(JSON.parse(contents));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw invalidCredentialFile();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
write(value) {
|
|
100
|
+
const parsed = credentialFileSchema.parse(value);
|
|
101
|
+
const directory = path.dirname(this.filePath);
|
|
102
|
+
mkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
103
|
+
chmodPrivate(directory, PRIVATE_DIRECTORY_MODE);
|
|
104
|
+
const temporaryPath = path.join(directory, `.${path.basename(this.filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(temporaryPath, `${JSON.stringify(parsed, null, 2)}\n`, {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
mode: PRIVATE_FILE_MODE,
|
|
109
|
+
});
|
|
110
|
+
renameSync(temporaryPath, this.filePath);
|
|
111
|
+
chmodPrivate(this.filePath, PRIVATE_FILE_MODE);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
rmSync(temporaryPath, { force: true });
|
|
115
|
+
throw credentialStorageError();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function resolvePaseoHome(env) {
|
|
120
|
+
const configured = env.PASEO_HOME ?? "~/.paseo";
|
|
121
|
+
const expanded = configured === "~" ? homedir() : configured.replace(/^~\//u, `${homedir()}/`);
|
|
122
|
+
return path.resolve(expanded);
|
|
123
|
+
}
|
|
124
|
+
function chmodPrivate(target, mode) {
|
|
125
|
+
if (process.platform === "win32")
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
chmodSync(target, mode);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw credentialStorageError();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isMissingFile(error) {
|
|
135
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
136
|
+
}
|
|
137
|
+
function invalidCredentialFile() {
|
|
138
|
+
return new HubCommandError("HUB_CREDENTIALS_INVALID", "Stored Hub login is invalid. Run `paseo hub login <origin>` to replace it.");
|
|
139
|
+
}
|
|
140
|
+
function credentialStorageError() {
|
|
141
|
+
return new HubCommandError("HUB_CREDENTIALS_UNAVAILABLE", "Could not access the private Hub credential store under PASEO_HOME.");
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface HubStatus {
|
|
2
|
+
state: string;
|
|
3
|
+
daemonId: string | null;
|
|
4
|
+
hubOrigin: string | null;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
connectedAt: string | null;
|
|
7
|
+
lastError: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface HubDaemonClient {
|
|
10
|
+
connectHub(url: string, token: string): Promise<{
|
|
11
|
+
status: HubStatus;
|
|
12
|
+
}>;
|
|
13
|
+
getHubStatus(): Promise<{
|
|
14
|
+
status: HubStatus;
|
|
15
|
+
}>;
|
|
16
|
+
disconnectHub(force: boolean): Promise<{
|
|
17
|
+
status: HubStatus;
|
|
18
|
+
warning?: string;
|
|
19
|
+
}>;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface HubDaemonConnection {
|
|
23
|
+
connect(host: string | undefined): Promise<HubDaemonClient>;
|
|
24
|
+
}
|
|
25
|
+
export declare const productionHubDaemonConnection: HubDaemonConnection;
|
|
26
|
+
export declare function withHubDaemon<T>(connection: HubDaemonConnection, host: string | undefined, action: (client: HubDaemonClient) => Promise<T>): Promise<T>;
|
|
27
|
+
//# sourceMappingURL=daemon-client.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { connectToDaemon } from "../../utils/client.js";
|
|
2
|
+
export const productionHubDaemonConnection = {
|
|
3
|
+
connect: (host) => connectToDaemon({ host }),
|
|
4
|
+
};
|
|
5
|
+
export async function withHubDaemon(connection, host, action) {
|
|
6
|
+
const client = await connection.connect(host);
|
|
7
|
+
try {
|
|
8
|
+
return await action(client);
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
await client.close().catch(() => undefined);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=daemon-client.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface HubBundleFile {
|
|
2
|
+
path: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HubDeployBundle {
|
|
6
|
+
projectSlug: string;
|
|
7
|
+
files: readonly HubBundleFile[];
|
|
8
|
+
workflowCount: number;
|
|
9
|
+
}
|
|
10
|
+
interface DiscoverHubBundleInput {
|
|
11
|
+
cwd: string;
|
|
12
|
+
project?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function discoverHubBundle(input: DiscoverHubBundleInput): Promise<HubDeployBundle>;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=deploy-bundle.d.ts.map
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
|
+
import { HubCommandError } from "./error.js";
|
|
5
|
+
const HUB_RESOURCE_PATH = ".paseo/hub.yml";
|
|
6
|
+
const LEGACY_TOML_PATH = ".paseo/hub.toml";
|
|
7
|
+
const WORKFLOW_DIRECTORY = ".paseo/workflows";
|
|
8
|
+
const PARTIAL_DIRECTORY = `${WORKFLOW_DIRECTORY}/partials`;
|
|
9
|
+
const PROJECT_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
10
|
+
export async function discoverHubBundle(input) {
|
|
11
|
+
const projectSlug = requireProjectSlug(input.project);
|
|
12
|
+
const root = path.resolve(input.cwd);
|
|
13
|
+
await rejectLegacyToml(root);
|
|
14
|
+
const resource = await readBundleFile(root, HUB_RESOURCE_PATH, {
|
|
15
|
+
missingCode: "HUB_RESOURCE_MISSING",
|
|
16
|
+
missingMessage: `${HUB_RESOURCE_PATH} does not exist. Run this command from the project root.`,
|
|
17
|
+
});
|
|
18
|
+
const workflowNames = await discoverWorkflowNames(root);
|
|
19
|
+
const workflows = await Promise.all(workflowNames.map((name) => readBundleFile(root, `${WORKFLOW_DIRECTORY}/${name}`)));
|
|
20
|
+
const partialPaths = collectPartialPaths(workflows);
|
|
21
|
+
const partials = await Promise.all(partialPaths.map((file) => readBundleFile(root, file)));
|
|
22
|
+
const files = [resource, ...workflows, ...partials].toSorted(compareBundleFiles);
|
|
23
|
+
assertUniqueBundlePaths(files);
|
|
24
|
+
return { projectSlug, files, workflowCount: workflows.length };
|
|
25
|
+
}
|
|
26
|
+
function requireProjectSlug(project) {
|
|
27
|
+
if (project === undefined) {
|
|
28
|
+
throw new HubCommandError("HUB_PROJECT_REQUIRED", "Project is required. Pass --project <slug>.");
|
|
29
|
+
}
|
|
30
|
+
if (!PROJECT_SLUG_PATTERN.test(project)) {
|
|
31
|
+
throw new HubCommandError("HUB_INVALID_PROJECT", "Project must be a bare slug such as my-project.");
|
|
32
|
+
}
|
|
33
|
+
return project;
|
|
34
|
+
}
|
|
35
|
+
async function rejectLegacyToml(root) {
|
|
36
|
+
try {
|
|
37
|
+
await lstat(path.join(root, LEGACY_TOML_PATH));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (errorCode(error) === "ENOENT")
|
|
41
|
+
return;
|
|
42
|
+
throw unreadablePath(LEGACY_TOML_PATH);
|
|
43
|
+
}
|
|
44
|
+
throw new HubCommandError("HUB_CONFIGURATION_EXTENSION_UNSUPPORTED", `${LEGACY_TOML_PATH} is not supported. Use ${HUB_RESOURCE_PATH}.`);
|
|
45
|
+
}
|
|
46
|
+
async function discoverWorkflowNames(root) {
|
|
47
|
+
const directory = path.join(root, WORKFLOW_DIRECTORY);
|
|
48
|
+
await requireSafeDirectory(root, directory, WORKFLOW_DIRECTORY, {
|
|
49
|
+
missingCode: "HUB_WORKFLOW_DIRECTORY_MISSING",
|
|
50
|
+
missingMessage: `${WORKFLOW_DIRECTORY} does not exist.`,
|
|
51
|
+
});
|
|
52
|
+
let entries;
|
|
53
|
+
try {
|
|
54
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw unreadablePath(WORKFLOW_DIRECTORY);
|
|
58
|
+
}
|
|
59
|
+
const names = [];
|
|
60
|
+
for (const entry of entries.toSorted((left, right) => compareText(left.name, right.name))) {
|
|
61
|
+
const bundlePath = `${WORKFLOW_DIRECTORY}/${entry.name}`;
|
|
62
|
+
if (entry.isSymbolicLink())
|
|
63
|
+
throw unsafePath(bundlePath);
|
|
64
|
+
if (entry.isDirectory()) {
|
|
65
|
+
if (entry.name === "partials")
|
|
66
|
+
continue;
|
|
67
|
+
throw new HubCommandError("HUB_WORKFLOW_PATH_UNSUPPORTED", `${bundlePath} is not a direct-child .yml workflow.`);
|
|
68
|
+
}
|
|
69
|
+
if (!entry.isFile())
|
|
70
|
+
throw regularFileRequired(bundlePath);
|
|
71
|
+
if (!entry.name.endsWith(".yml")) {
|
|
72
|
+
throw new HubCommandError("HUB_WORKFLOW_EXTENSION_UNSUPPORTED", `${bundlePath} must use the .yml extension.`);
|
|
73
|
+
}
|
|
74
|
+
names.push(entry.name);
|
|
75
|
+
}
|
|
76
|
+
if (names.length === 0) {
|
|
77
|
+
throw new HubCommandError("HUB_WORKFLOW_MISSING", `${WORKFLOW_DIRECTORY} must contain at least one direct-child .yml workflow.`);
|
|
78
|
+
}
|
|
79
|
+
return names;
|
|
80
|
+
}
|
|
81
|
+
function collectPartialPaths(workflows) {
|
|
82
|
+
const paths = new Set();
|
|
83
|
+
for (const workflow of workflows) {
|
|
84
|
+
let document;
|
|
85
|
+
try {
|
|
86
|
+
document = YAML.parse(workflow.content);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new HubCommandError("HUB_WORKFLOW_INVALID_YAML", `${workflow.path} is not valid YAML.`);
|
|
90
|
+
}
|
|
91
|
+
if (!isRecord(document) || !Array.isArray(document["steps"]))
|
|
92
|
+
continue;
|
|
93
|
+
for (const step of document["steps"]) {
|
|
94
|
+
if (!isRecord(step) || !Array.isArray(step["prompt"]))
|
|
95
|
+
continue;
|
|
96
|
+
for (const block of step["prompt"]) {
|
|
97
|
+
if (!isRecord(block) || !Object.hasOwn(block, "include"))
|
|
98
|
+
continue;
|
|
99
|
+
const include = block["include"];
|
|
100
|
+
if (typeof include !== "string") {
|
|
101
|
+
throw invalidPartialPath(workflow.path, String(include));
|
|
102
|
+
}
|
|
103
|
+
paths.add(resolvePartialPath(workflow.path, include));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return [...paths].sort(compareText);
|
|
108
|
+
}
|
|
109
|
+
function resolvePartialPath(workflowPath, include) {
|
|
110
|
+
const decoded = decodePartialPath(workflowPath, include);
|
|
111
|
+
if (/^(?:[a-zA-Z]:[\\/]|[\\/]{1,2})/u.test(decoded)) {
|
|
112
|
+
throw invalidPartialPath(workflowPath, include);
|
|
113
|
+
}
|
|
114
|
+
const segments = decoded.replaceAll("\\", "/").split("/");
|
|
115
|
+
if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..") ||
|
|
116
|
+
segments[0] !== "partials") {
|
|
117
|
+
throw invalidPartialPath(workflowPath, include);
|
|
118
|
+
}
|
|
119
|
+
return `${WORKFLOW_DIRECTORY}/${segments.join("/")}`;
|
|
120
|
+
}
|
|
121
|
+
function decodePartialPath(workflowPath, include) {
|
|
122
|
+
let decoded = include;
|
|
123
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
124
|
+
let next;
|
|
125
|
+
try {
|
|
126
|
+
next = decodeURIComponent(decoded);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw invalidPartialPath(workflowPath, include);
|
|
130
|
+
}
|
|
131
|
+
if (next === decoded)
|
|
132
|
+
break;
|
|
133
|
+
decoded = next;
|
|
134
|
+
}
|
|
135
|
+
if (decoded.includes("\0") || /%[0-9a-f]{2}/iu.test(decoded)) {
|
|
136
|
+
throw invalidPartialPath(workflowPath, include);
|
|
137
|
+
}
|
|
138
|
+
return decoded;
|
|
139
|
+
}
|
|
140
|
+
async function readBundleFile(root, bundlePath, missing = {
|
|
141
|
+
missingCode: "HUB_BUNDLE_FILE_MISSING",
|
|
142
|
+
missingMessage: `${bundlePath} does not exist.`,
|
|
143
|
+
}) {
|
|
144
|
+
const absolutePath = path.join(root, ...bundlePath.split("/"));
|
|
145
|
+
await rejectSymlinkComponents(root, absolutePath, bundlePath);
|
|
146
|
+
let stats;
|
|
147
|
+
try {
|
|
148
|
+
stats = await lstat(absolutePath);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (errorCode(error) === "ENOENT") {
|
|
152
|
+
throw new HubCommandError(missing.missingCode, missing.missingMessage);
|
|
153
|
+
}
|
|
154
|
+
throw unreadablePath(bundlePath);
|
|
155
|
+
}
|
|
156
|
+
if (stats.isSymbolicLink())
|
|
157
|
+
throw unsafePath(bundlePath);
|
|
158
|
+
if (!stats.isFile())
|
|
159
|
+
throw regularFileRequired(bundlePath);
|
|
160
|
+
if (!hasReadPermission(stats.mode))
|
|
161
|
+
throw unreadablePath(bundlePath);
|
|
162
|
+
try {
|
|
163
|
+
return { path: bundlePath, content: (await readFile(absolutePath)).toString("utf8") };
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw unreadablePath(bundlePath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function requireSafeDirectory(root, absolutePath, bundlePath, missing) {
|
|
170
|
+
await rejectSymlinkComponents(root, absolutePath, bundlePath);
|
|
171
|
+
let stats;
|
|
172
|
+
try {
|
|
173
|
+
stats = await lstat(absolutePath);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (errorCode(error) === "ENOENT") {
|
|
177
|
+
throw new HubCommandError(missing.missingCode, missing.missingMessage);
|
|
178
|
+
}
|
|
179
|
+
throw unreadablePath(bundlePath);
|
|
180
|
+
}
|
|
181
|
+
if (stats.isSymbolicLink())
|
|
182
|
+
throw unsafePath(bundlePath);
|
|
183
|
+
if (!stats.isDirectory()) {
|
|
184
|
+
throw new HubCommandError("HUB_WORKFLOW_DIRECTORY_INVALID", `${bundlePath} must be a directory.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function rejectSymlinkComponents(root, target, bundlePath) {
|
|
188
|
+
const relative = path.relative(root, target);
|
|
189
|
+
let current = root;
|
|
190
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
191
|
+
current = path.join(current, segment);
|
|
192
|
+
try {
|
|
193
|
+
if ((await lstat(current)).isSymbolicLink())
|
|
194
|
+
throw unsafePath(bundlePath);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof HubCommandError)
|
|
198
|
+
throw error;
|
|
199
|
+
if (errorCode(error) === "ENOENT")
|
|
200
|
+
return;
|
|
201
|
+
throw unreadablePath(bundlePath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function assertUniqueBundlePaths(files) {
|
|
206
|
+
const seen = new Set();
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
if (seen.has(file.path)) {
|
|
209
|
+
throw new HubCommandError("HUB_BUNDLE_PATH_DUPLICATE", `${file.path} appears more than once in the discovered bundle.`);
|
|
210
|
+
}
|
|
211
|
+
seen.add(file.path);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function invalidPartialPath(workflowPath, include) {
|
|
215
|
+
return new HubCommandError("HUB_PARTIAL_PATH_INVALID", `${workflowPath} includes an unsafe path outside ${PARTIAL_DIRECTORY}: ${include}`);
|
|
216
|
+
}
|
|
217
|
+
function unsafePath(bundlePath) {
|
|
218
|
+
return new HubCommandError("HUB_BUNDLE_UNSAFE_PATH", `${bundlePath} must not use a symlink.`);
|
|
219
|
+
}
|
|
220
|
+
function unreadablePath(bundlePath) {
|
|
221
|
+
return new HubCommandError("HUB_BUNDLE_UNREADABLE", `Could not read ${bundlePath}. Check the file and permissions.`);
|
|
222
|
+
}
|
|
223
|
+
function regularFileRequired(bundlePath) {
|
|
224
|
+
return new HubCommandError("HUB_BUNDLE_NOT_FILE", `${bundlePath} must be a regular file.`);
|
|
225
|
+
}
|
|
226
|
+
function compareBundleFiles(left, right) {
|
|
227
|
+
return compareText(left.path, right.path);
|
|
228
|
+
}
|
|
229
|
+
function compareText(left, right) {
|
|
230
|
+
if (left < right)
|
|
231
|
+
return -1;
|
|
232
|
+
if (left > right)
|
|
233
|
+
return 1;
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
function hasReadPermission(mode) {
|
|
237
|
+
return (mode & 0o444) !== 0;
|
|
238
|
+
}
|
|
239
|
+
function errorCode(error) {
|
|
240
|
+
if (typeof error !== "object" || error === null)
|
|
241
|
+
return undefined;
|
|
242
|
+
const code = Reflect.get(error, "code");
|
|
243
|
+
return typeof code === "string" ? code : undefined;
|
|
244
|
+
}
|
|
245
|
+
function isRecord(value) {
|
|
246
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=deploy-bundle.js.map
|
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { type SingleResult } from "../../output/index.js";
|
|
3
|
-
import { type HubInstallResult } from "./client.js";
|
|
3
|
+
import { HubHttpClient, type HubInstallResult, type HubValidationResult } from "./hub-client/index.js";
|
|
4
|
+
import { type HubCredentialStore } from "./credentials.js";
|
|
5
|
+
import { type HubReporter } from "./reporter.js";
|
|
4
6
|
export interface HubDeployOptions {
|
|
5
|
-
file?: string;
|
|
6
7
|
project?: string;
|
|
7
8
|
hub?: string;
|
|
8
9
|
apiKey?: string;
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
json?: boolean;
|
|
9
12
|
}
|
|
10
13
|
interface HubDeployEnvironment {
|
|
11
14
|
cwd: string;
|
|
12
15
|
env: Readonly<Record<string, string | undefined>>;
|
|
16
|
+
credentials?: HubCredentialStore;
|
|
17
|
+
hub?: Pick<HubHttpClient, "installConfiguration" | "validateConfiguration">;
|
|
18
|
+
reporter?: HubReporter;
|
|
13
19
|
}
|
|
14
|
-
export
|
|
15
|
-
|
|
20
|
+
export interface HubDeployCommandDependencies {
|
|
21
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
22
|
+
credentials: HubCredentialStore;
|
|
23
|
+
hub: Pick<HubHttpClient, "installConfiguration" | "validateConfiguration">;
|
|
24
|
+
reporter: HubReporter;
|
|
25
|
+
cwd(): string;
|
|
26
|
+
}
|
|
27
|
+
interface HubDeployResult extends HubInstallResult {
|
|
28
|
+
origin: string;
|
|
29
|
+
workflows: number;
|
|
30
|
+
}
|
|
31
|
+
interface HubDryRunResult extends HubValidationResult {
|
|
32
|
+
origin: string;
|
|
33
|
+
workflows: number;
|
|
34
|
+
}
|
|
35
|
+
export declare function runHubDeploy(options: HubDeployOptions, environment?: HubDeployEnvironment): Promise<SingleResult<HubDeployResult> | SingleResult<HubDryRunResult>>;
|
|
36
|
+
export declare function addHubDeployCommand(hub: Command, dependencies: HubDeployCommandDependencies): void;
|
|
16
37
|
export {};
|
|
17
38
|
//# sourceMappingURL=deploy.d.ts.map
|