@web42/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +86 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +27 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +171 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.js +198 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +22 -0
- package/dist/commands/pack.d.ts +2 -0
- package/dist/commands/pack.js +80 -0
- package/dist/commands/pull.d.ts +2 -0
- package/dist/commands/pull.js +63 -0
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +127 -0
- package/dist/commands/remix.d.ts +2 -0
- package/dist/commands/remix.js +49 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +58 -0
- package/dist/commands/uninstall.d.ts +3 -0
- package/dist/commands/uninstall.js +54 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +59 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +37 -0
- package/dist/platforms/base.d.ts +58 -0
- package/dist/platforms/base.js +1 -0
- package/dist/platforms/openclaw/adapter.d.ts +10 -0
- package/dist/platforms/openclaw/adapter.js +452 -0
- package/dist/platforms/openclaw/templates.d.ts +7 -0
- package/dist/platforms/openclaw/templates.js +369 -0
- package/dist/platforms/registry.d.ts +6 -0
- package/dist/platforms/registry.js +30 -0
- package/dist/utils/api.d.ts +3 -0
- package/dist/utils/api.js +35 -0
- package/dist/utils/config.d.ts +22 -0
- package/dist/utils/config.js +50 -0
- package/dist/utils/secrets.d.ts +32 -0
- package/dist/utils/secrets.js +118 -0
- package/dist/utils/skill.d.ts +4 -0
- package/dist/utils/skill.js +21 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { apiPost } from "../utils/api.js";
|
|
6
|
+
import { clearAuth, getConfig, setAuth } from "../utils/config.js";
|
|
7
|
+
export const authCommand = new Command("auth").description("Authenticate with the Web42 marketplace");
|
|
8
|
+
authCommand
|
|
9
|
+
.command("login")
|
|
10
|
+
.description("Sign in via GitHub OAuth in the browser")
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
const code = randomBytes(16).toString("hex");
|
|
14
|
+
const spinner = ora("Registering auth code...").start();
|
|
15
|
+
try {
|
|
16
|
+
await apiPost("/api/auth/cli", { action: "register", code });
|
|
17
|
+
spinner.stop();
|
|
18
|
+
const loginUrl = `${config.apiUrl}/login?cli_code=${code}`;
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.bold("Open this URL in your browser to authenticate:"));
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(chalk.cyan(loginUrl));
|
|
23
|
+
console.log();
|
|
24
|
+
// Try to open browser automatically
|
|
25
|
+
const { exec } = await import("child_process");
|
|
26
|
+
const platform = process.platform;
|
|
27
|
+
const openCmd = platform === "darwin"
|
|
28
|
+
? "open"
|
|
29
|
+
: platform === "win32"
|
|
30
|
+
? "start"
|
|
31
|
+
: "xdg-open";
|
|
32
|
+
exec(`${openCmd} "${loginUrl}"`);
|
|
33
|
+
const pollSpinner = ora("Waiting for authentication...").start();
|
|
34
|
+
// Poll for confirmation
|
|
35
|
+
const maxAttempts = 60;
|
|
36
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
38
|
+
try {
|
|
39
|
+
const result = await apiPost("/api/auth/cli", { action: "poll", code });
|
|
40
|
+
if (result.status === "authenticated" && result.user_id && result.token) {
|
|
41
|
+
pollSpinner.succeed("Authenticated!");
|
|
42
|
+
setAuth({
|
|
43
|
+
userId: result.user_id,
|
|
44
|
+
username: result.username ?? "",
|
|
45
|
+
token: result.token,
|
|
46
|
+
fullName: result.full_name,
|
|
47
|
+
avatarUrl: result.avatar_url,
|
|
48
|
+
});
|
|
49
|
+
console.log();
|
|
50
|
+
console.log(chalk.green(`Logged in as ${chalk.bold(`@${result.username}`)}`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Continue polling
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
pollSpinner.fail("Authentication timed out. Please try again.");
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
spinner.fail("Failed to start auth flow");
|
|
62
|
+
console.error(error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
authCommand
|
|
67
|
+
.command("logout")
|
|
68
|
+
.description("Sign out and clear saved credentials")
|
|
69
|
+
.action(() => {
|
|
70
|
+
clearAuth();
|
|
71
|
+
console.log(chalk.green("Logged out successfully."));
|
|
72
|
+
});
|
|
73
|
+
authCommand
|
|
74
|
+
.command("whoami")
|
|
75
|
+
.description("Show the currently authenticated user")
|
|
76
|
+
.action(() => {
|
|
77
|
+
const config = getConfig();
|
|
78
|
+
if (!config.authenticated || !config.username) {
|
|
79
|
+
console.log(chalk.yellow("Not authenticated. Run `web42 auth login`."));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
console.log(chalk.green(`@${config.username}`));
|
|
83
|
+
if (config.fullName) {
|
|
84
|
+
console.log(chalk.dim(config.fullName));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { getConfig, setApiUrl } from "../utils/config.js";
|
|
4
|
+
export const configCommand = new Command("config").description("View or update CLI configuration");
|
|
5
|
+
configCommand
|
|
6
|
+
.command("show")
|
|
7
|
+
.description("Show the current configuration")
|
|
8
|
+
.action(() => {
|
|
9
|
+
const cfg = getConfig();
|
|
10
|
+
console.log(chalk.bold("Web42 CLI Configuration"));
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(` API URL: ${chalk.cyan(cfg.apiUrl)}`);
|
|
13
|
+
console.log(` Authenticated: ${cfg.authenticated ? chalk.green("yes") : chalk.yellow("no")}`);
|
|
14
|
+
if (cfg.username) {
|
|
15
|
+
console.log(` User: ${chalk.cyan(`@${cfg.username}`)}`);
|
|
16
|
+
}
|
|
17
|
+
if (cfg.fullName) {
|
|
18
|
+
console.log(` Name: ${cfg.fullName}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
configCommand
|
|
22
|
+
.command("set-url <url>")
|
|
23
|
+
.description("Persistently set the API URL (e.g. http://localhost:3000)")
|
|
24
|
+
.action((url) => {
|
|
25
|
+
setApiUrl(url);
|
|
26
|
+
console.log(chalk.green(`API URL set to ${chalk.bold(url)}`));
|
|
27
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { writeFileSync, existsSync, readdirSync, readFileSync, } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import { requireAuth } from "../utils/config.js";
|
|
7
|
+
import { parseSkillMd } from "../utils/skill.js";
|
|
8
|
+
import { AGENTS_MD, IDENTITY_MD, SOUL_MD, TOOLS_MD, USER_MD, HEARTBEAT_MD, INIT_BOOTSTRAP_MD, } from "../platforms/openclaw/templates.js";
|
|
9
|
+
function detectWorkspaceSkills(cwd) {
|
|
10
|
+
const skillsDir = join(cwd, "skills");
|
|
11
|
+
if (!existsSync(skillsDir))
|
|
12
|
+
return [];
|
|
13
|
+
const skills = [];
|
|
14
|
+
try {
|
|
15
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
const skillMd = join(skillsDir, entry.name, "SKILL.md");
|
|
19
|
+
if (existsSync(skillMd)) {
|
|
20
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
21
|
+
const parsed = parseSkillMd(content, entry.name);
|
|
22
|
+
skills.push(parsed);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// ignore read errors
|
|
29
|
+
}
|
|
30
|
+
return skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
31
|
+
}
|
|
32
|
+
const CATEGORIES = [
|
|
33
|
+
"Customer Support",
|
|
34
|
+
"Healthcare",
|
|
35
|
+
"Developer Tools",
|
|
36
|
+
"Personal Assistant",
|
|
37
|
+
"Sales",
|
|
38
|
+
"Marketing",
|
|
39
|
+
"Education",
|
|
40
|
+
"Finance",
|
|
41
|
+
"Content Creation",
|
|
42
|
+
"Productivity",
|
|
43
|
+
];
|
|
44
|
+
export const initCommand = new Command("init")
|
|
45
|
+
.description("Create a manifest.json for your agent package")
|
|
46
|
+
.action(async () => {
|
|
47
|
+
const config = requireAuth();
|
|
48
|
+
const cwd = process.cwd();
|
|
49
|
+
const manifestPath = join(cwd, "manifest.json");
|
|
50
|
+
if (existsSync(manifestPath)) {
|
|
51
|
+
const { overwrite } = await inquirer.prompt([
|
|
52
|
+
{
|
|
53
|
+
type: "confirm",
|
|
54
|
+
name: "overwrite",
|
|
55
|
+
message: "manifest.json already exists. Overwrite?",
|
|
56
|
+
default: false,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
if (!overwrite) {
|
|
60
|
+
console.log(chalk.yellow("Aborted."));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const answers = await inquirer.prompt([
|
|
65
|
+
{
|
|
66
|
+
type: "list",
|
|
67
|
+
name: "platform",
|
|
68
|
+
message: "Platform:",
|
|
69
|
+
choices: ["openclaw"],
|
|
70
|
+
default: "openclaw",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: "input",
|
|
74
|
+
name: "name",
|
|
75
|
+
message: "Agent name (lowercase, hyphens allowed):",
|
|
76
|
+
validate: (val) => /^[a-z0-9][a-z0-9-]*$/.test(val) ||
|
|
77
|
+
"Must be lowercase alphanumeric with hyphens",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "input",
|
|
81
|
+
name: "description",
|
|
82
|
+
message: "Short description:",
|
|
83
|
+
validate: (val) => val.length > 0 && val.length <= 500 || "1-500 characters",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "input",
|
|
87
|
+
name: "version",
|
|
88
|
+
message: "Version:",
|
|
89
|
+
default: "1.0.0",
|
|
90
|
+
validate: (val) => /^\d+\.\d+\.\d+$/.test(val) || "Must follow semver (e.g. 1.0.0)",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: "list",
|
|
94
|
+
name: "category",
|
|
95
|
+
message: "Primary category:",
|
|
96
|
+
choices: CATEGORIES,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: "input",
|
|
100
|
+
name: "tags",
|
|
101
|
+
message: "Tags (comma-separated):",
|
|
102
|
+
filter: (val) => val
|
|
103
|
+
.split(",")
|
|
104
|
+
.map((t) => t.trim())
|
|
105
|
+
.filter(Boolean),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "input",
|
|
109
|
+
name: "primaryModel",
|
|
110
|
+
message: "Primary model preference (e.g. claude-sonnet-4-20250514):",
|
|
111
|
+
default: "",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: "input",
|
|
115
|
+
name: "demoVideoUrl",
|
|
116
|
+
message: "Demo video URL (optional):",
|
|
117
|
+
default: "",
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
const detectedSkills = detectWorkspaceSkills(cwd);
|
|
121
|
+
if (detectedSkills.length > 0) {
|
|
122
|
+
console.log(chalk.dim(` Detected ${detectedSkills.length} skill(s): ${detectedSkills.map((s) => s.name).join(", ")}`));
|
|
123
|
+
}
|
|
124
|
+
const manifest = {
|
|
125
|
+
format: "agentpkg/1",
|
|
126
|
+
platform: answers.platform,
|
|
127
|
+
name: answers.name,
|
|
128
|
+
description: answers.description,
|
|
129
|
+
version: answers.version,
|
|
130
|
+
author: config.username,
|
|
131
|
+
skills: detectedSkills,
|
|
132
|
+
plugins: [],
|
|
133
|
+
modelPreferences: answers.primaryModel
|
|
134
|
+
? { primary: answers.primaryModel }
|
|
135
|
+
: undefined,
|
|
136
|
+
tags: answers.tags,
|
|
137
|
+
demoVideoUrl: answers.demoVideoUrl || undefined,
|
|
138
|
+
configVariables: [],
|
|
139
|
+
};
|
|
140
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.green(`Created ${chalk.bold("manifest.json")}`));
|
|
143
|
+
const scaffoldFiles = [
|
|
144
|
+
{ name: "AGENTS.md", content: AGENTS_MD, alwaysWrite: false },
|
|
145
|
+
{ name: "IDENTITY.md", content: IDENTITY_MD, alwaysWrite: false },
|
|
146
|
+
{ name: "SOUL.md", content: SOUL_MD, alwaysWrite: false },
|
|
147
|
+
{ name: "TOOLS.md", content: TOOLS_MD, alwaysWrite: false },
|
|
148
|
+
{ name: "HEARTBEAT.md", content: HEARTBEAT_MD, alwaysWrite: false },
|
|
149
|
+
{ name: "BOOTSTRAP.md", content: INIT_BOOTSTRAP_MD, alwaysWrite: false },
|
|
150
|
+
{ name: "USER.md", content: USER_MD, alwaysWrite: true },
|
|
151
|
+
];
|
|
152
|
+
const created = [];
|
|
153
|
+
const skipped = [];
|
|
154
|
+
for (const file of scaffoldFiles) {
|
|
155
|
+
const filePath = join(cwd, file.name);
|
|
156
|
+
if (!file.alwaysWrite && existsSync(filePath)) {
|
|
157
|
+
skipped.push(file.name);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
writeFileSync(filePath, file.content, "utf-8");
|
|
161
|
+
created.push(file.name);
|
|
162
|
+
}
|
|
163
|
+
if (created.length > 0) {
|
|
164
|
+
console.log(chalk.green(` Scaffolded: ${created.join(", ")}`));
|
|
165
|
+
}
|
|
166
|
+
if (skipped.length > 0) {
|
|
167
|
+
console.log(chalk.dim(` Skipped (already exist): ${skipped.join(", ")}`));
|
|
168
|
+
}
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(chalk.dim("Run `web42 pack` to bundle your agent, or `web42 push` to pack and publish."));
|
|
171
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { apiGet, apiPost } from "../utils/api.js";
|
|
8
|
+
function deriveProviderEnvKey(model) {
|
|
9
|
+
const slash = model.indexOf("/");
|
|
10
|
+
if (slash < 1)
|
|
11
|
+
return null;
|
|
12
|
+
const provider = model.slice(0, slash);
|
|
13
|
+
const envKey = `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
14
|
+
return { provider, envKey };
|
|
15
|
+
}
|
|
16
|
+
function isKeyConfigured(envKey, platformHome) {
|
|
17
|
+
if (process.env[envKey])
|
|
18
|
+
return true;
|
|
19
|
+
const dotenvPath = join(platformHome, ".env");
|
|
20
|
+
if (!existsSync(dotenvPath))
|
|
21
|
+
return false;
|
|
22
|
+
try {
|
|
23
|
+
const content = readFileSync(dotenvPath, "utf-8");
|
|
24
|
+
return content.split("\n").some((line) => {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (trimmed.startsWith("#"))
|
|
27
|
+
return false;
|
|
28
|
+
const eqIdx = trimmed.indexOf("=");
|
|
29
|
+
return eqIdx > 0 && trimmed.slice(0, eqIdx).trim() === envKey;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function appendToEnv(envKey, value, platformHome) {
|
|
37
|
+
const dotenvPath = join(platformHome, ".env");
|
|
38
|
+
const line = `${envKey}=${value}\n`;
|
|
39
|
+
appendFileSync(dotenvPath, line, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
export function makeInstallCommand(adapter) {
|
|
42
|
+
return new Command("install")
|
|
43
|
+
.description("Install an agent package from the marketplace")
|
|
44
|
+
.argument("<agent>", "Agent to install (e.g. @user/agent-name)")
|
|
45
|
+
.option("--as <name>", "Install under a different local agent name")
|
|
46
|
+
.option("--no-prompt", "Skip config variable prompts, use defaults")
|
|
47
|
+
.action(async (agentRef, opts) => {
|
|
48
|
+
const match = agentRef.match(/^@?([^/]+)\/(.+)$/);
|
|
49
|
+
if (!match) {
|
|
50
|
+
console.log(chalk.red("Invalid agent reference. Use @user/agent-name format."));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
const [, username, agentSlug] = match;
|
|
54
|
+
const spinner = ora(`Fetching @${username}/${agentSlug}...`).start();
|
|
55
|
+
try {
|
|
56
|
+
const agents = await apiGet(`/api/agents?username=${username}`);
|
|
57
|
+
const agent = agents.find((a) => a.slug === agentSlug);
|
|
58
|
+
if (!agent) {
|
|
59
|
+
spinner.fail(`Agent @${username}/${agentSlug} not found`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = await apiPost(`/api/agents/${agent.id}/install`, {});
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (err.message?.includes("Access required") &&
|
|
68
|
+
agent.price_cents > 0) {
|
|
69
|
+
const siteUrl = process.env.WEB42_API_URL ?? "https://marketplace.web42.ai";
|
|
70
|
+
spinner.fail(`This is a paid agent ($${(agent.price_cents / 100).toFixed(2)}). Purchase it on the web first:`);
|
|
71
|
+
console.log(chalk.cyan(` ${siteUrl}/${username}/${agentSlug}`));
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
const manifest = result.agent.manifest;
|
|
77
|
+
let configAnswers = {};
|
|
78
|
+
if (manifest.configVariables && manifest.configVariables.length > 0) {
|
|
79
|
+
if (opts.prompt === false) {
|
|
80
|
+
for (const v of manifest.configVariables) {
|
|
81
|
+
configAnswers[v.key] = v.default ?? "";
|
|
82
|
+
}
|
|
83
|
+
spinner.text = "Installing agent (skipping prompts)...";
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
spinner.stop();
|
|
87
|
+
console.log();
|
|
88
|
+
console.log(chalk.bold("Configure your agent:"));
|
|
89
|
+
console.log();
|
|
90
|
+
configAnswers = await inquirer.prompt(manifest.configVariables.map((v) => ({
|
|
91
|
+
type: "input",
|
|
92
|
+
name: v.key,
|
|
93
|
+
message: `${v.label}${v.description ? ` (${v.description})` : ""}:`,
|
|
94
|
+
default: v.default,
|
|
95
|
+
validate: (val) => !v.required || val.length > 0 || `${v.label} is required`,
|
|
96
|
+
})));
|
|
97
|
+
spinner.start("Installing agent...");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
spinner.text = "Installing agent...";
|
|
102
|
+
}
|
|
103
|
+
const primaryModel = manifest.modelPreferences?.primary;
|
|
104
|
+
if (primaryModel) {
|
|
105
|
+
const providerInfo = deriveProviderEnvKey(primaryModel);
|
|
106
|
+
if (providerInfo) {
|
|
107
|
+
if (isKeyConfigured(providerInfo.envKey, adapter.home)) {
|
|
108
|
+
if (spinner.isSpinning)
|
|
109
|
+
spinner.stop();
|
|
110
|
+
console.log(chalk.dim(` ${providerInfo.envKey} already configured for ${primaryModel}`));
|
|
111
|
+
if (!spinner.isSpinning)
|
|
112
|
+
spinner.start("Installing agent...");
|
|
113
|
+
}
|
|
114
|
+
else if (opts.prompt === false) {
|
|
115
|
+
if (spinner.isSpinning)
|
|
116
|
+
spinner.stop();
|
|
117
|
+
console.log();
|
|
118
|
+
console.log(chalk.yellow(` This agent uses ${chalk.bold(primaryModel)}. You'll need to set ${chalk.bold(providerInfo.envKey)} in ${adapter.home}/.env or as an environment variable.`));
|
|
119
|
+
spinner.start("Installing agent...");
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
if (spinner.isSpinning)
|
|
123
|
+
spinner.stop();
|
|
124
|
+
console.log();
|
|
125
|
+
console.log(chalk.bold(`This agent uses ${chalk.cyan(primaryModel)}.`));
|
|
126
|
+
const { apiKey } = await inquirer.prompt([
|
|
127
|
+
{
|
|
128
|
+
type: "password",
|
|
129
|
+
name: "apiKey",
|
|
130
|
+
message: `Enter your ${providerInfo.envKey} (leave empty to skip):`,
|
|
131
|
+
mask: "*",
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
if (apiKey) {
|
|
135
|
+
appendToEnv(providerInfo.envKey, apiKey, adapter.home);
|
|
136
|
+
console.log(chalk.green(` Saved ${providerInfo.envKey} to ${adapter.home}/.env`));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(chalk.yellow(` Skipped. Set ${chalk.bold(providerInfo.envKey)} in ${adapter.home}/.env later.`));
|
|
140
|
+
}
|
|
141
|
+
spinner.start("Installing agent...");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let configTemplate = null;
|
|
146
|
+
const configFile = result.files.find((f) => f.path === ".openclaw/config.json" && f.content);
|
|
147
|
+
if (configFile?.content) {
|
|
148
|
+
try {
|
|
149
|
+
configTemplate = JSON.parse(configFile.content);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// No config template available
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const localName = opts.as ?? agentSlug;
|
|
156
|
+
const workspacePath = join(adapter.home, `workspace-${localName}`);
|
|
157
|
+
const installResult = await adapter.install({
|
|
158
|
+
agentSlug: localName,
|
|
159
|
+
username,
|
|
160
|
+
workspacePath,
|
|
161
|
+
files: result.files,
|
|
162
|
+
configTemplate,
|
|
163
|
+
configAnswers,
|
|
164
|
+
});
|
|
165
|
+
const web42Config = {
|
|
166
|
+
source: `@${username}/${agentSlug}`,
|
|
167
|
+
...configAnswers,
|
|
168
|
+
};
|
|
169
|
+
const configPath = join(workspacePath, ".web42.config.json");
|
|
170
|
+
writeFileSync(configPath, JSON.stringify(web42Config, null, 2) + "\n");
|
|
171
|
+
spinner.stop();
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(chalk.green(`Installed ${chalk.bold(`@${username}/${agentSlug}`)} as agent "${localName}"`));
|
|
174
|
+
console.log(chalk.dim(` Workspace: ${workspacePath}`));
|
|
175
|
+
if (manifest.skills && manifest.skills.length > 0) {
|
|
176
|
+
console.log(chalk.dim(` Skills: ${manifest.skills.join(", ")}`));
|
|
177
|
+
}
|
|
178
|
+
console.log(chalk.dim(` ${installResult.filesWritten} files written`));
|
|
179
|
+
const pendingVars = (manifest.configVariables ?? []).filter((v) => v.required && !configAnswers[v.key]);
|
|
180
|
+
if (pendingVars.length > 0) {
|
|
181
|
+
console.log();
|
|
182
|
+
console.log(chalk.yellow(` ${pendingVars.length} config variable(s) still need setup:`));
|
|
183
|
+
for (const v of pendingVars) {
|
|
184
|
+
console.log(chalk.yellow(` - ${v.label} (${v.key})`));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.log();
|
|
188
|
+
console.log(chalk.dim(" Next steps:"));
|
|
189
|
+
console.log(chalk.dim(` 1. Set up channel bindings: ${adapter.name} config`));
|
|
190
|
+
console.log(chalk.dim(` 2. Restart the gateway: ${adapter.name} gateway restart`));
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
spinner.fail("Install failed");
|
|
194
|
+
console.error(chalk.red(error.message));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
export function makeListCommand(adapter) {
|
|
4
|
+
return new Command("list")
|
|
5
|
+
.description("List installed agents")
|
|
6
|
+
.action(async () => {
|
|
7
|
+
const agents = await adapter.listInstalled();
|
|
8
|
+
if (agents.length === 0) {
|
|
9
|
+
console.log(chalk.yellow("No agents installed."));
|
|
10
|
+
console.log(chalk.dim(`Run \`web42 ${adapter.name} install @user/agent\` to install one.`));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(chalk.bold(`Installed ${adapter.name} agents:`));
|
|
14
|
+
console.log();
|
|
15
|
+
for (const agent of agents) {
|
|
16
|
+
console.log(` ${chalk.cyan(agent.source ?? agent.name)}`);
|
|
17
|
+
if (agent.workspace) {
|
|
18
|
+
console.log(chalk.dim(` ${agent.workspace}`));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { openclawAdapter } from "../platforms/openclaw/adapter.js";
|
|
7
|
+
import { parseSkillMd } from "../utils/skill.js";
|
|
8
|
+
export const packCommand = new Command("pack")
|
|
9
|
+
.description("Pack your agent workspace into a distributable artifact")
|
|
10
|
+
.option("-o, --output <dir>", "Output directory", ".web42")
|
|
11
|
+
.option("--dry-run", "Preview what would be packed without writing files")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const manifestPath = join(cwd, "manifest.json");
|
|
15
|
+
if (!existsSync(manifestPath)) {
|
|
16
|
+
console.log(chalk.red("No manifest.json found. Run `web42 init` first."));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
20
|
+
if (!manifest.name || !manifest.version || !manifest.author) {
|
|
21
|
+
console.log(chalk.red("Invalid manifest.json. Must have name, version, and author."));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const spinner = ora("Packing agent...").start();
|
|
25
|
+
try {
|
|
26
|
+
const result = await openclawAdapter.pack({ cwd, outputDir: opts.output });
|
|
27
|
+
// Detect skills from packed files (skills/*/SKILL.md pattern)
|
|
28
|
+
const detectedSkills = [];
|
|
29
|
+
for (const f of result.files) {
|
|
30
|
+
const match = f.path.match(/^skills\/([^/]+)\/SKILL\.md$/);
|
|
31
|
+
if (match) {
|
|
32
|
+
const parsed = parseSkillMd(f.content, match[1]);
|
|
33
|
+
detectedSkills.push(parsed);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (detectedSkills.length > 0) {
|
|
37
|
+
manifest.skills = detectedSkills.sort((a, b) => a.name.localeCompare(b.name));
|
|
38
|
+
}
|
|
39
|
+
// Merge auto-generated config variables into manifest
|
|
40
|
+
const existingKeys = new Set((manifest.configVariables ?? []).map((v) => v.key));
|
|
41
|
+
for (const cv of result.configVariables) {
|
|
42
|
+
if (!existingKeys.has(cv.key)) {
|
|
43
|
+
if (!manifest.configVariables)
|
|
44
|
+
manifest.configVariables = [];
|
|
45
|
+
manifest.configVariables.push(cv);
|
|
46
|
+
existingKeys.add(cv.key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (opts.dryRun) {
|
|
50
|
+
spinner.stop();
|
|
51
|
+
console.log(chalk.bold("Dry run — would pack:"));
|
|
52
|
+
console.log();
|
|
53
|
+
for (const f of result.files) {
|
|
54
|
+
console.log(chalk.dim(` ${f.path} (${f.content.length} bytes)`));
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
console.log(chalk.dim(`${result.files.length} files, ${result.configVariables.length} config variable(s)`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const outputDir = join(cwd, opts.output);
|
|
61
|
+
mkdirSync(outputDir, { recursive: true });
|
|
62
|
+
for (const file of result.files) {
|
|
63
|
+
const filePath = join(outputDir, file.path);
|
|
64
|
+
mkdirSync(join(filePath, ".."), { recursive: true });
|
|
65
|
+
writeFileSync(filePath, file.content, "utf-8");
|
|
66
|
+
}
|
|
67
|
+
writeFileSync(join(outputDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
68
|
+
spinner.succeed(`Packed ${chalk.bold(manifest.name)} (${result.files.length} files) → ${opts.output}/`);
|
|
69
|
+
if (result.configVariables.length > 0) {
|
|
70
|
+
console.log(chalk.dim(` ${result.configVariables.length} config variable(s) detected`));
|
|
71
|
+
}
|
|
72
|
+
console.log();
|
|
73
|
+
console.log(chalk.dim("Run `web42 push` to publish to the marketplace."));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
spinner.fail("Pack failed");
|
|
77
|
+
console.error(chalk.red(error.message));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|