Skip to content

Commit fd62b41

Browse files
committed
feat: auto-detect upgrades in setup cli
1 parent 1b8d56c commit fd62b41

6 files changed

Lines changed: 144 additions & 9 deletions

File tree

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ npx -y @aitoearn/openclaw-plugin
1111
这个命令会自动:
1212

1313
1. 将当前插件包安装到 OpenClaw 的扩展目录
14-
2. 引导输入 API Key 和环境
15-
3. 验证连通性
16-
4. 写入 `plugins.entries.aitoearn.config`
14+
2. 自动检测是否为已有安装
15+
3. 首次安装时引导输入 API Key 和环境
16+
4. 验证连通性
17+
5. 写入 `plugins.entries.aitoearn.config`
1718

1819
## 手动配置
1920

@@ -51,6 +52,8 @@ npx -y @aitoearn/openclaw-plugin
5152
npx -y @aitoearn/openclaw-plugin upgrade
5253
```
5354

55+
再次执行默认命令时,如果检测到已有 `aitoearn` 配置,会自动按升级处理并跳过 setup。
56+
5457
完成配置后,重启 Gateway:
5558

5659
```bash

openclaw.plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "aitoearn",
33
"name": "AiToEarn",
44
"description": "AiToEarn social media management tools",
5-
"version": "1.0.5",
5+
"version": "1.0.6",
66
"configSchema": {
77
"type": "object",
88
"additionalProperties": false,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@aitoearn/openclaw-plugin",
3-
"version": "1.0.5",
3+
"version": "1.0.6",
44
"type": "module",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",

src/cli.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,43 @@ describe("runSetupCli", () => {
115115
expect(prompts.outro).toHaveBeenCalled();
116116
});
117117

118+
it("auto-detects upgrade when existing plugin config is present", async () => {
119+
installPlugin.mockResolvedValue({
120+
replacedExisting: true,
121+
targetDir: "/tmp/.openclaw/extensions/aitoearn",
122+
});
123+
readConfig.mockResolvedValue({
124+
plugins: {
125+
entries: {
126+
aitoearn: {
127+
enabled: true,
128+
config: {
129+
apiKey: "existing-api-key",
130+
baseUrl: "https://aitoearn.ai/api",
131+
},
132+
},
133+
},
134+
},
135+
});
136+
137+
const exitCode = await runSetupCli([], {
138+
prompts,
139+
loadPackageContext,
140+
installPlugin,
141+
readConfig,
142+
writeConfig,
143+
runSetupFlow,
144+
});
145+
146+
expect(exitCode).toBe(0);
147+
expect(readConfig).toHaveBeenCalledTimes(1);
148+
expect(runSetupFlow).not.toHaveBeenCalled();
149+
expect(writeConfig).not.toHaveBeenCalled();
150+
expect(prompts.outro).toHaveBeenCalledWith(
151+
'Existing configuration detected. Upgrade complete! Run "openclaw gateway restart" to apply.'
152+
);
153+
});
154+
118155
it("returns success when setup is cancelled after installation", async () => {
119156
runSetupFlow.mockResolvedValue({ status: "cancelled" });
120157

@@ -131,6 +168,57 @@ describe("runSetupCli", () => {
131168
expect(writeConfig).not.toHaveBeenCalled();
132169
});
133170

171+
it("continues into setup when existing installation has no saved apiKey", async () => {
172+
installPlugin.mockResolvedValue({
173+
replacedExisting: true,
174+
targetDir: "/tmp/.openclaw/extensions/aitoearn",
175+
});
176+
readConfig.mockResolvedValue({
177+
plugins: {
178+
entries: {
179+
aitoearn: {
180+
enabled: true,
181+
config: {},
182+
},
183+
},
184+
},
185+
});
186+
runSetupFlow.mockResolvedValue({
187+
status: "completed",
188+
config: {
189+
apiKey: "new-api-key",
190+
baseUrl: "https://aitoearn.ai/api/",
191+
},
192+
toolCount: 3,
193+
});
194+
195+
const exitCode = await runSetupCli([], {
196+
prompts,
197+
loadPackageContext,
198+
installPlugin,
199+
readConfig,
200+
writeConfig,
201+
runSetupFlow,
202+
});
203+
204+
expect(exitCode).toBe(0);
205+
expect(readConfig).toHaveBeenCalledTimes(1);
206+
expect(runSetupFlow).toHaveBeenCalled();
207+
expect(writeConfig).toHaveBeenCalledWith({
208+
plugins: {
209+
entries: {
210+
aitoearn: {
211+
enabled: true,
212+
config: {
213+
apiKey: "new-api-key",
214+
baseUrl: "https://aitoearn.ai/api",
215+
},
216+
},
217+
},
218+
},
219+
});
220+
});
221+
134222
it("upgrades plugin files without rerunning setup", async () => {
135223
installPlugin.mockResolvedValue({
136224
replacedExisting: true,

src/cli.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
runInteractiveSetupFlow,
1818
type SetupFlowResult,
1919
} from "./setup-flow.js";
20+
import { configSchema, PLUGIN_ID } from "./plugin-config.js";
2021

2122
interface SpinnerApi {
2223
start(message: string): void;
@@ -67,7 +68,7 @@ export async function runSetupCli(
6768
return 1;
6869
}
6970

70-
const command = args[0] ?? "install";
71+
const command = args[0] ?? "auto";
7172

7273
deps.prompts.intro("AiToEarn OpenClaw Setup");
7374

@@ -104,6 +105,23 @@ export async function runSetupCli(
104105
return 0;
105106
}
106107

108+
let currentConfig: Record<string, unknown> | null = null;
109+
if (command === "auto" && installResult.replacedExisting) {
110+
try {
111+
currentConfig = await deps.readConfig();
112+
} catch (error) {
113+
deps.prompts.cancel(formatError(error));
114+
return 1;
115+
}
116+
117+
if (hasConfiguredPluginEntry(currentConfig)) {
118+
deps.prompts.outro(
119+
'Existing configuration detected. Upgrade complete! Run "openclaw gateway restart" to apply.'
120+
);
121+
return 0;
122+
}
123+
}
124+
107125
const setupResult = await deps.runSetupFlow({ showIntro: false });
108126
if (setupResult.status === "cancelled") {
109127
return 0;
@@ -117,7 +135,7 @@ export async function runSetupCli(
117135
configSpinner.start("Writing OpenClaw configuration...");
118136

119137
try {
120-
const currentConfig = await deps.readConfig();
138+
currentConfig ??= await deps.readConfig();
121139
const nextConfig = applySetupConfigToOpenClawConfig(
122140
currentConfig,
123141
setupResult.config
@@ -153,6 +171,32 @@ function formatError(error: unknown): string {
153171
return error instanceof Error ? error.message : String(error);
154172
}
155173

174+
function hasConfiguredPluginEntry(config: Record<string, unknown>): boolean {
175+
const plugins = asRecord(config.plugins);
176+
const entries = asRecord(plugins?.entries);
177+
const pluginEntry = asRecord(entries?.[PLUGIN_ID]);
178+
const parsed = configSchema.safeParse(pluginEntry?.config ?? {});
179+
180+
if (!parsed.success) {
181+
return false;
182+
}
183+
184+
const apiKey = parsed.data.apiKey;
185+
if (typeof apiKey === "string") {
186+
return apiKey.trim().length > 0;
187+
}
188+
189+
return isRecord(apiKey);
190+
}
191+
192+
function asRecord(value: unknown): Record<string, unknown> | null {
193+
return isRecord(value) ? value : null;
194+
}
195+
196+
function isRecord(value: unknown): value is Record<string, unknown> {
197+
return typeof value === "object" && value !== null && !Array.isArray(value);
198+
}
199+
156200
async function main(): Promise<void> {
157201
const exitCode = await runSetupCli(process.argv.slice(2));
158202
if (exitCode !== 0) {

0 commit comments

Comments
 (0)