clawport-ui 0.8.5 → 0.8.7
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/components/OnboardingWizard.tsx +13 -4
- package/lib/cli-utils.test.ts +16 -0
- package/lib/cli-utils.ts +25 -12
- package/lib/crons.test.ts +14 -7
- package/lib/crons.ts +19 -9
- package/package.json +1 -1
|
@@ -164,15 +164,24 @@ export function OnboardingWizard({ forceOpen, onClose }: OnboardingWizardProps)
|
|
|
164
164
|
|
|
165
165
|
// Check crons (validates gateway + openclaw binary)
|
|
166
166
|
fetch('/api/crons')
|
|
167
|
-
.then(r => {
|
|
168
|
-
if (!r.ok)
|
|
167
|
+
.then(async r => {
|
|
168
|
+
if (!r.ok) {
|
|
169
|
+
const body = await r.json().catch(() => null)
|
|
170
|
+
const serverMsg = body?.error
|
|
171
|
+
throw new Error(serverMsg || `HTTP ${r.status}`)
|
|
172
|
+
}
|
|
169
173
|
return r.json()
|
|
170
174
|
})
|
|
171
175
|
.then(() => {
|
|
172
176
|
setCronsStatus('ok')
|
|
173
177
|
})
|
|
174
|
-
.catch(() => {
|
|
175
|
-
|
|
178
|
+
.catch((err: Error) => {
|
|
179
|
+
const msg = err.message || ''
|
|
180
|
+
if (msg.includes('Failed to fetch cron') || msg.includes('JSON') || msg.includes('Unexpected token')) {
|
|
181
|
+
setCronsError('Cron list failed -- OpenClaw CLI may be printing log output before JSON. Try: OPENCLAW_LOG_LEVEL=error clawport dev')
|
|
182
|
+
} else {
|
|
183
|
+
setCronsError('Could not reach OpenClaw gateway. Run: openclaw gateway run')
|
|
184
|
+
}
|
|
176
185
|
setCronsStatus('error')
|
|
177
186
|
})
|
|
178
187
|
}
|
package/lib/cli-utils.test.ts
CHANGED
|
@@ -41,4 +41,20 @@ hooks: Unrecognized key: "allowedAgentIds"
|
|
|
41
41
|
it('throws on empty string', () => {
|
|
42
42
|
expect(() => extractJson('')).toThrow()
|
|
43
43
|
})
|
|
44
|
+
|
|
45
|
+
it('skips bracketed log lines like [plugins] before real JSON', () => {
|
|
46
|
+
const raw = `[plugins] [debug] Database schema initialized
|
|
47
|
+
[plugins] [debug] Loading context engine
|
|
48
|
+
{"jobs":[{"name":"daily-report","schedule":"0 8 * * *"}]}`
|
|
49
|
+
const result = extractJson(raw) as Record<string, unknown>
|
|
50
|
+
expect(result.jobs).toEqual([{ name: 'daily-report', schedule: '0 8 * * *' }])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('skips bracketed log lines before JSON array', () => {
|
|
54
|
+
const raw = `[plugins] [debug] Database schema initialized
|
|
55
|
+
[plugins] [info] Ready
|
|
56
|
+
[{"id":"pulse","name":"daily-pulse"}]`
|
|
57
|
+
const result = extractJson(raw)
|
|
58
|
+
expect(result).toEqual([{ id: 'pulse', name: 'daily-pulse' }])
|
|
59
|
+
})
|
|
44
60
|
})
|
package/lib/cli-utils.ts
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extract a JSON value from CLI output that may contain non-JSON preamble.
|
|
3
3
|
*
|
|
4
|
-
* Some OpenClaw versions print validation warnings
|
|
5
|
-
* to stdout before the JSON payload.
|
|
6
|
-
*
|
|
4
|
+
* Some OpenClaw versions print validation warnings or debug log lines
|
|
5
|
+
* (e.g. "[plugins] [debug] ...") to stdout before the JSON payload.
|
|
6
|
+
* This function finds the actual JSON structure by trying each `[` or `{`
|
|
7
|
+
* position until one parses successfully.
|
|
7
8
|
*/
|
|
8
9
|
export function extractJson(raw: string): unknown {
|
|
9
10
|
// Fast path: raw is already valid JSON
|
|
10
11
|
const trimmed = raw.trim()
|
|
11
12
|
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
12
|
-
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(trimmed)
|
|
15
|
+
} catch {
|
|
16
|
+
// May start with [ but be a log line like "[plugins] ..." -- fall through
|
|
17
|
+
}
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// Try each potential JSON start position
|
|
21
|
+
let pos = 0
|
|
22
|
+
while (pos < raw.length) {
|
|
23
|
+
const arrStart = raw.indexOf('[', pos)
|
|
24
|
+
const objStart = raw.indexOf('{', pos)
|
|
25
|
+
const candidates = [arrStart, objStart].filter(i => i >= 0)
|
|
26
|
+
if (candidates.length === 0) break
|
|
27
|
+
|
|
28
|
+
const start = Math.min(...candidates)
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(raw.slice(start))
|
|
31
|
+
} catch {
|
|
32
|
+
// This wasn't the real JSON start -- advance past it
|
|
33
|
+
pos = start + 1
|
|
34
|
+
}
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
return JSON.parse(raw.slice(start))
|
|
37
|
+
throw new SyntaxError('No JSON found in CLI output')
|
|
25
38
|
}
|
package/lib/crons.test.ts
CHANGED
|
@@ -254,18 +254,25 @@ describe('getCrons - error and lastError', () => {
|
|
|
254
254
|
})
|
|
255
255
|
})
|
|
256
256
|
|
|
257
|
-
// ---
|
|
257
|
+
// --- Graceful degradation (returns empty array on failure) ---
|
|
258
258
|
|
|
259
|
-
describe('getCrons -
|
|
260
|
-
it('
|
|
259
|
+
describe('getCrons - graceful degradation', () => {
|
|
260
|
+
it('returns empty array when execSync throws (CLI not installed)', async () => {
|
|
261
261
|
mockExecSync.mockImplementation(() => { throw new Error('ENOENT') })
|
|
262
|
-
await
|
|
263
|
-
|
|
262
|
+
const result = await getCrons()
|
|
263
|
+
expect(result).toEqual([])
|
|
264
264
|
})
|
|
265
265
|
|
|
266
|
-
it('
|
|
266
|
+
it('returns empty array for invalid JSON output', async () => {
|
|
267
267
|
mockExecSync.mockReturnValue('not valid json {{')
|
|
268
|
-
await
|
|
268
|
+
const result = await getCrons()
|
|
269
|
+
expect(result).toEqual([])
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('returns empty array when OPENCLAW_BIN is not set', async () => {
|
|
273
|
+
vi.unstubAllEnvs()
|
|
274
|
+
const result = await getCrons()
|
|
275
|
+
expect(result).toEqual([])
|
|
269
276
|
})
|
|
270
277
|
})
|
|
271
278
|
|
package/lib/crons.ts
CHANGED
|
@@ -32,11 +32,22 @@ export async function getCrons(): Promise<CronJob[]> {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
|
-
const openclawBin =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
35
|
+
const openclawBin = process.env.OPENCLAW_BIN
|
|
36
|
+
if (!openclawBin) {
|
|
37
|
+
// No binary configured -- return empty list instead of crashing
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let raw: string
|
|
42
|
+
try {
|
|
43
|
+
raw = execSync(`${openclawBin} cron list --json`, {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
timeout: 10000,
|
|
46
|
+
})
|
|
47
|
+
} catch {
|
|
48
|
+
// CLI failed (binary not found, no crons, gateway down) -- return empty
|
|
49
|
+
return []
|
|
50
|
+
}
|
|
40
51
|
|
|
41
52
|
const parsed = extractJson(raw) as Record<string, unknown>
|
|
42
53
|
const jobs: unknown[] = Array.isArray(parsed)
|
|
@@ -113,9 +124,8 @@ export async function getCrons(): Promise<CronJob[]> {
|
|
|
113
124
|
|
|
114
125
|
_cronsCache = { result, ts: Date.now() }
|
|
115
126
|
return result
|
|
116
|
-
} catch
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
127
|
+
} catch {
|
|
128
|
+
// JSON extraction or parsing failed -- return empty rather than 500
|
|
129
|
+
return []
|
|
120
130
|
}
|
|
121
131
|
}
|