@danwo/cli 0.1.0 → 0.1.2
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/package.json +11 -11
- package/src/bin.ts +33 -10
- package/src/doctor.ts +104 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danwo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Command-line interface.",
|
|
5
5
|
"homepage": "https://danwo.dev",
|
|
6
6
|
"bugs": "https://github.com/carbonmeai/danwo/issues",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"postpack": "node ../../scripts/pack-files.ts remove"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@danwo/dashboard": "0.1.
|
|
28
|
-
"@danwo/github": "0.1.
|
|
29
|
-
"@danwo/init": "0.1.
|
|
30
|
-
"@danwo/mcp": "0.1.
|
|
31
|
-
"@danwo/mobile": "0.1.
|
|
32
|
-
"@danwo/recorder": "0.1.
|
|
33
|
-
"@danwo/runner": "0.1.
|
|
34
|
-
"@danwo/skill": "0.1.
|
|
27
|
+
"@danwo/dashboard": "0.1.2",
|
|
28
|
+
"@danwo/github": "0.1.2",
|
|
29
|
+
"@danwo/init": "0.1.2",
|
|
30
|
+
"@danwo/mcp": "0.1.2",
|
|
31
|
+
"@danwo/mobile": "0.1.2",
|
|
32
|
+
"@danwo/recorder": "0.1.2",
|
|
33
|
+
"@danwo/runner": "0.1.2",
|
|
34
|
+
"@danwo/skill": "0.1.2",
|
|
35
35
|
"commander": "15.0.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"@bachs/sdk": "1.0.0",
|
|
40
40
|
"@clerk/express": "2.1.80",
|
|
41
41
|
"@composio/core": "0.25.0",
|
|
42
|
-
"@danwo/fidelity": "0.1.
|
|
43
|
-
"@danwo/sdk": "0.1.
|
|
42
|
+
"@danwo/fidelity": "0.1.2",
|
|
43
|
+
"@danwo/sdk": "0.1.2",
|
|
44
44
|
"@hocuspocus/provider": "4.7.1",
|
|
45
45
|
"@nangohq/node": "0.71.13",
|
|
46
46
|
"@novu/api": "3.19.1",
|
package/src/bin.ts
CHANGED
|
@@ -114,10 +114,14 @@ const reporterOption = () =>
|
|
|
114
114
|
"reporters besides the console's list: github (a check run, a pull request comment and the job summary in GitHub Actions), junit (.danwo/junit.xml)",
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
-
/** `--reporter`, and `--no-pr-comment` for the github reporter. */
|
|
117
|
+
/** `--reporter`, `--name`, and `--no-pr-comment` for the github reporter. */
|
|
118
118
|
function withReporterOptions(command: Command): Command {
|
|
119
119
|
return command
|
|
120
120
|
.addOption(reporterOption())
|
|
121
|
+
.option(
|
|
122
|
+
"--name <suite>",
|
|
123
|
+
"the suite's name, for a repository with several suites: the github reporter's check run, pull request comment section, summary and artifact (default: the config's name)",
|
|
124
|
+
)
|
|
121
125
|
.option("--no-pr-comment", "github reporter: no pull request comment, only the check run")
|
|
122
126
|
}
|
|
123
127
|
|
|
@@ -151,6 +155,10 @@ withReporterOptions(withLintOptions(program.command("test")))
|
|
|
151
155
|
duration,
|
|
152
156
|
)
|
|
153
157
|
.option("--ci", "CI defaults even without the CI variable: 1 retry, read-only cache, no .only")
|
|
158
|
+
.option(
|
|
159
|
+
"--no-ci",
|
|
160
|
+
"local defaults even with CI set (no retry, a writable cache, .only allowed), e.g. for a run started from inside another",
|
|
161
|
+
)
|
|
154
162
|
.option(
|
|
155
163
|
"--replay-only",
|
|
156
164
|
"replay recorded agent steps and call no model: a step with no recording that replays fails",
|
|
@@ -196,6 +204,7 @@ withReporterOptions(withLintOptions(program.command("test")))
|
|
|
196
204
|
updateSnapshots?: boolean
|
|
197
205
|
reporter?: string[]
|
|
198
206
|
prComment?: boolean
|
|
207
|
+
name?: string
|
|
199
208
|
lint?: boolean
|
|
200
209
|
},
|
|
201
210
|
) => {
|
|
@@ -277,6 +286,7 @@ withReporterOptions(withLintOptions(program.command("matrix")))
|
|
|
277
286
|
updateSnapshots?: boolean
|
|
278
287
|
reporter?: string[]
|
|
279
288
|
prComment?: boolean
|
|
289
|
+
name?: string
|
|
280
290
|
lint?: boolean
|
|
281
291
|
},
|
|
282
292
|
) => {
|
|
@@ -291,13 +301,19 @@ withReporterOptions(program.command("merge-reports"))
|
|
|
291
301
|
.description("join the results of a split run (danwo test --shard i/n) into one report")
|
|
292
302
|
.argument("[files...]", "results files (default: .danwo/results/shard-*.json)")
|
|
293
303
|
.option("-c, --config <path>", "config file (default: nearest danwo.config.ts)")
|
|
294
|
-
.action(
|
|
295
|
-
|
|
296
|
-
files,
|
|
297
|
-
...
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
304
|
+
.action(
|
|
305
|
+
async (
|
|
306
|
+
files: string[],
|
|
307
|
+
{ name, ...options }: { config?: string; name?: string } & ReporterFlags,
|
|
308
|
+
) => {
|
|
309
|
+
process.exitCode = await mergeReports({
|
|
310
|
+
files,
|
|
311
|
+
...(options.config !== undefined && { config: options.config }),
|
|
312
|
+
...(name !== undefined && { name }),
|
|
313
|
+
reporters: reportersOf(options),
|
|
314
|
+
})
|
|
315
|
+
},
|
|
316
|
+
)
|
|
301
317
|
|
|
302
318
|
program
|
|
303
319
|
.command("impact")
|
|
@@ -384,7 +400,7 @@ program
|
|
|
384
400
|
program
|
|
385
401
|
.command("doctor")
|
|
386
402
|
.description(
|
|
387
|
-
"check what a first run needs: Node, danwo's Chromium and the config's other browsers, the config and its keys, the model's key, Playwright Test's version and the agents' skill",
|
|
403
|
+
"check what a first run needs: Node, danwo's Chromium and the config's other browsers, the config and its keys, the model's key, the device toolchain for a mobile config, Playwright Test's version and the agents' skill",
|
|
388
404
|
)
|
|
389
405
|
.option("--json", "the checks as JSON: { ok, checks: [{ name, status, detail }] }")
|
|
390
406
|
.option("-c, --config <path>", "config file (default: nearest danwo.config.ts)")
|
|
@@ -393,7 +409,14 @@ program
|
|
|
393
409
|
const { browserInstalled } = await import("@danwo/runner")
|
|
394
410
|
const checks = await projectDoctor(
|
|
395
411
|
{ cwd: process.cwd(), config: options.config },
|
|
396
|
-
{
|
|
412
|
+
{
|
|
413
|
+
nodeVersion: process.version,
|
|
414
|
+
env: process.env,
|
|
415
|
+
browser: browserInstalled,
|
|
416
|
+
device: async (platform) =>
|
|
417
|
+
(await import("@danwo/mobile")).deviceDoctor(undefined, { platform }),
|
|
418
|
+
flutter: async () => (await import("@danwo/mobile")).flutterDoctor(),
|
|
419
|
+
},
|
|
397
420
|
)
|
|
398
421
|
const ok = checks.every((check) => check.status !== "fail")
|
|
399
422
|
|
package/src/doctor.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* `danwo doctor`: what stops a first run, checked before one. Node, danwo's own Chromium (and
|
|
3
3
|
* the config's other browsers, when `browser.name` names Firefox or WebKit), the config (loading, and keys danwo does not know), the model's key (or, for a model on a coding CLI, the CLI's install and sign-in), the installed Playwright Test
|
|
4
4
|
* against `@danwo/playwright`'s range, and whether the coding agents' copy of the skill is this
|
|
5
|
-
* danwo's.
|
|
5
|
+
* danwo's. A config that runs its tests on devices (`withIosDevice`, `buildAndroid` and the other
|
|
6
|
+
* `@danwo/mobile` helpers) gets `danwo device doctor`'s checks for each platform it runs on, and
|
|
7
|
+
* Flutter's for a Flutter app.
|
|
6
8
|
*/
|
|
7
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
9
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
8
10
|
import { createRequire } from "node:module"
|
|
9
|
-
import { join } from "node:path"
|
|
11
|
+
import { dirname, join, resolve } from "node:path"
|
|
10
12
|
|
|
11
13
|
import { DANWO_VERSION } from "@danwo/init/catalog"
|
|
12
14
|
import {
|
|
@@ -25,6 +27,9 @@ export interface DoctorCheck {
|
|
|
25
27
|
readonly detail: string
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** A platform a mobile config runs its tests on. */
|
|
31
|
+
export type MobilePlatform = "android" | "ios"
|
|
32
|
+
|
|
28
33
|
/** What the checks read from the machine; replaced in tests. */
|
|
29
34
|
export interface DoctorProbes {
|
|
30
35
|
readonly nodeVersion: string
|
|
@@ -33,6 +38,10 @@ export interface DoctorProbes {
|
|
|
33
38
|
readonly browser: (
|
|
34
39
|
engine: BrowserName,
|
|
35
40
|
) => Promise<{ readonly ok: boolean; readonly detail: string }>
|
|
41
|
+
/** `danwo device doctor`'s checks for one platform, for a config that runs on devices. */
|
|
42
|
+
readonly device: (platform: MobilePlatform) => Promise<readonly DoctorCheck[]>
|
|
43
|
+
/** Flutter's check, for a project with a `pubspec.yaml`. */
|
|
44
|
+
readonly flutter: () => Promise<readonly DoctorCheck[]>
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/** The oldest Playwright Test `@danwo/playwright` takes. */
|
|
@@ -140,7 +149,7 @@ async function configChecks(
|
|
|
140
149
|
cwd: string,
|
|
141
150
|
explicit: string | undefined,
|
|
142
151
|
env: NodeJS.ProcessEnv,
|
|
143
|
-
): Promise<{ checks: DoctorCheck[]; root: string; config?: DanwoConfig }> {
|
|
152
|
+
): Promise<{ checks: DoctorCheck[]; root: string; config?: DanwoConfig; path?: string }> {
|
|
144
153
|
try {
|
|
145
154
|
const loaded = await loadConfig(cwd, explicit)
|
|
146
155
|
|
|
@@ -165,6 +174,7 @@ async function configChecks(
|
|
|
165
174
|
return {
|
|
166
175
|
root: loaded.root,
|
|
167
176
|
config: loaded.config,
|
|
177
|
+
path: resolve(cwd, loaded.path),
|
|
168
178
|
checks: [
|
|
169
179
|
{ name: "config", status: "ok", detail: loaded.path },
|
|
170
180
|
...keys,
|
|
@@ -228,6 +238,95 @@ function skillChecks(root: string): DoctorCheck[] {
|
|
|
228
238
|
})
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
/** The `@danwo/mobile` helpers that put a config's tests on each platform's devices. */
|
|
242
|
+
const DEVICE_HELPERS: Readonly<Record<MobilePlatform, RegExp>> = {
|
|
243
|
+
ios: /\b(withIosDevice|buildIos|useSimulator|iosDevice)\b/,
|
|
244
|
+
android: /\b(withAndroidDevice|buildAndroid|useEmulator|androidDevice)\b/,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const TITLES: Readonly<Record<MobilePlatform, string>> = { ios: "iOS", android: "Android" }
|
|
248
|
+
|
|
249
|
+
/** A file's text, or nothing when it cannot be read. */
|
|
250
|
+
function textOf(file: string): string {
|
|
251
|
+
try {
|
|
252
|
+
return readFileSync(file, "utf8")
|
|
253
|
+
} catch {
|
|
254
|
+
return ""
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The platforms a config runs on, from the device helpers its source calls: the config file's own,
|
|
260
|
+
* and the local modules it imports, such as a `tests/stack/device.ts` holding the binding.
|
|
261
|
+
*/
|
|
262
|
+
function mobilePlatformsOf(configFile: string): MobilePlatform[] {
|
|
263
|
+
const own = textOf(configFile)
|
|
264
|
+
const imported = [...own.matchAll(/from\s+["'](\.{1,2}\/[^"']+)["']/g)].map((match) =>
|
|
265
|
+
textOf(resolve(dirname(configFile), match[1] ?? "")),
|
|
266
|
+
)
|
|
267
|
+
const source = [own, ...imported].join("\n")
|
|
268
|
+
|
|
269
|
+
return (["ios", "android"] as const).filter((platform) => DEVICE_HELPERS[platform].test(source))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Whether the project holds a Flutter app: a `pubspec.yaml` at its root or one folder down. */
|
|
273
|
+
function hasFlutterApp(root: string): boolean {
|
|
274
|
+
if (existsSync(join(root, "pubspec.yaml"))) return true
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
return readdirSync(root, { withFileTypes: true }).some(
|
|
278
|
+
(entry) =>
|
|
279
|
+
entry.isDirectory() &&
|
|
280
|
+
!entry.name.startsWith(".") &&
|
|
281
|
+
entry.name !== "node_modules" &&
|
|
282
|
+
existsSync(join(root, entry.name, "pubspec.yaml")),
|
|
283
|
+
)
|
|
284
|
+
} catch {
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The device checks a mobile config needs: each platform it runs on, and Flutter for a Flutter
|
|
291
|
+
* app. A config that runs on both platforms passes on a machine ready for one; the other's
|
|
292
|
+
* failures are warnings that say which platform they hold back.
|
|
293
|
+
*/
|
|
294
|
+
async function deviceChecks(
|
|
295
|
+
configFile: string | undefined,
|
|
296
|
+
root: string,
|
|
297
|
+
probes: DoctorProbes,
|
|
298
|
+
): Promise<DoctorCheck[]> {
|
|
299
|
+
const platforms = configFile === undefined ? [] : mobilePlatformsOf(configFile)
|
|
300
|
+
|
|
301
|
+
if (platforms.length === 0) return []
|
|
302
|
+
|
|
303
|
+
const found = await Promise.all(
|
|
304
|
+
platforms.map(async (platform) => ({ platform, checks: await probes.device(platform) })),
|
|
305
|
+
)
|
|
306
|
+
const ready = found.some(({ checks }) => checks.every((check) => check.status !== "fail"))
|
|
307
|
+
const devices = found.flatMap(({ platform, checks }) =>
|
|
308
|
+
checks.map((check): DoctorCheck =>
|
|
309
|
+
check.status === "fail" && ready && platforms.length > 1
|
|
310
|
+
? {
|
|
311
|
+
name: check.name,
|
|
312
|
+
status: "warn",
|
|
313
|
+
detail: `${check.detail} (needed only to run on ${TITLES[platform]})`,
|
|
314
|
+
}
|
|
315
|
+
: { name: check.name, status: check.status, detail: check.detail },
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return [
|
|
320
|
+
{
|
|
321
|
+
name: "devices",
|
|
322
|
+
status: "ok",
|
|
323
|
+
detail: `the config runs on ${platforms.map((platform) => TITLES[platform]).join(" and ")}`,
|
|
324
|
+
},
|
|
325
|
+
...devices,
|
|
326
|
+
...(hasFlutterApp(root) ? await probes.flutter() : []),
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
|
|
231
330
|
/** Every check, in the order a first run meets them. */
|
|
232
331
|
export async function projectDoctor(
|
|
233
332
|
options: { readonly cwd: string; readonly config?: string | undefined },
|
|
@@ -243,6 +342,7 @@ export async function projectDoctor(
|
|
|
243
342
|
{ name: "danwo", status: "ok", detail: DANWO_VERSION },
|
|
244
343
|
...browsers,
|
|
245
344
|
...config.checks,
|
|
345
|
+
...(await deviceChecks(config.path, config.root, probes)),
|
|
246
346
|
...playwrightCheck(config.root),
|
|
247
347
|
...skillChecks(config.root),
|
|
248
348
|
]
|