@danceiny/gotry 0.2.0-rc.31 → 0.2.0-rc.32
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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +63 -53
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/client/client.js +66 -21
- package/dist/capabilities/doctor.js +7 -8
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +8 -1
- package/dist/capabilities/session/gui-flight.js +737 -0
- package/dist/capabilities/session/gui-hotel.js +413 -0
- package/dist/capabilities/session-search.js +98 -11
- package/dist/scripts/bootstrap-tests.js +23 -6
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +19 -4
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +5 -0
- package/dist/scripts/gotry-web-client-tests.js +20 -1
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +1 -1
- package/dist/scripts/session-tests.js +177 -6
- package/dist/src/gotry-web-api.js +64 -2
- package/dist/src/index.js +40 -18
- package/extension/manifest.json +2 -2
- package/package.json +2 -1
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +9 -2
- package/ts/capabilities/session/gui-flight.ts +792 -0
- package/ts/capabilities/session/gui-hotel.ts +451 -0
- package/ts/capabilities/session-search.ts +115 -15
- package/ts/src/gotry-web-api.ts +51 -2
- package/ts/src/index.ts +41 -19
|
@@ -103,12 +103,22 @@ assert.ok(!c7b.out.includes('下载通道'), '非法值不应进入 github 通
|
|
|
103
103
|
console.log('7. 扩展分发 github 通道(不可达基址即时降级 + 非法值回落 bundled)OK');
|
|
104
104
|
// 8. doctor 子命令(2026-09-02 迪拜 session 复盘:可选依赖不撒手——doctor 统一
|
|
105
105
|
// 显示状态 + 精确补装指引;LLM key 显式让渡;报告落 gotry-state/doctor-report.md)
|
|
106
|
-
const
|
|
106
|
+
const doctorHome = mkdtempSync(join(tmpdir(), 'gotry-doctor-home-'));
|
|
107
|
+
let c8;
|
|
108
|
+
try {
|
|
109
|
+
c8 = runBootstrap(['doctor'], { HOME: doctorHome, FLYAI_API_KEY: '', DEBUG_FLYAI_API_KEY: '', STAICLI_HOME: join(doctorHome, '.staicli'), HOTELBYTE_TOKEN: '' });
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
rmSync(doctorHome, { recursive: true, force: true });
|
|
113
|
+
}
|
|
107
114
|
assert.ok(c8.out.includes('[gotry-doctor]'), '应输出 [gotry-doctor] 标签');
|
|
108
115
|
assert.ok(c8.out.includes('Agent Reach'), '体检应含 agent-reach 项');
|
|
109
116
|
assert.ok(c8.out.includes('hbcli'), '体检应含 hbcli 项');
|
|
110
117
|
assert.ok(c8.out.includes('FlyAI(飞猪官方检索'), '体检应含 flyai 项');
|
|
111
|
-
|
|
118
|
+
// FlyAI 面三合法态(doctor.ts #521:configured≠verified):匿名试用 / 已配置,未验证 / 已验证。
|
|
119
|
+
// 机器态依赖断言:本机配置并验证过 key 的环境(如 founder 机)doctor 输出「已验证 …」,
|
|
120
|
+
// 两态断言会把健康态误判为红(2026-10-14 发版门禁实测)。
|
|
121
|
+
assert.ok(c8.out.includes('匿名试用') || c8.out.includes('已配置,未验证') || c8.out.includes('已验证'), '体检应明确 FlyAI 当前状态(匿名试用/已配置,未验证/已验证)');
|
|
112
122
|
assert.ok(c8.out.includes('LLM key'), '体检应含 LLM key 让渡说明(doctor 不管 key)');
|
|
113
123
|
assert.ok(c8.out.includes('dsh-map-tools'), '体检应含 dsh-map-tools 项(#139;与 ts/capabilities/doctor.ts 两面成对)');
|
|
114
124
|
assert.ok(c8.out.includes('dsh-tool-ask-user'), '体检应含 dsh-tool-ask-user 项(与 ts/capabilities/doctor.ts 两面成对)');
|
|
@@ -396,6 +406,7 @@ console.log('18. onboarding CLI 跳过(非 TTY / CI / GOTRY_SETUP_SKIP / GOTRY_O
|
|
|
396
406
|
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(fixture, 'bin', 'gotry-flyai-verification.js'));
|
|
397
407
|
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(fixture, 'bin', 'gotry-process-liveness.js'));
|
|
398
408
|
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(fixture, 'bin', 'gotry-extension-status.js'));
|
|
409
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-hbcli-release.js'), join(fixture, 'bin', 'gotry-hbcli-release.js'));
|
|
399
410
|
copyFileSync(join(repoRoot, 'package.json'), join(fixture, 'package.json'));
|
|
400
411
|
const vendorDir = join(fixture, 'ts', 'dsh-runtime', 'vendor');
|
|
401
412
|
mkdirSync(vendorDir, { recursive: true });
|
|
@@ -647,6 +658,7 @@ function buildPkgFixture(tmpRoot) {
|
|
|
647
658
|
copyFileSync(join(repoRoot, 'bin', 'gotry-bootstrap.js'), join(pkgBin, 'gotry-bootstrap.js'));
|
|
648
659
|
copyFileSync(join(repoRoot, 'bin', 'gotry-flyai-verification.js'), join(pkgBin, 'gotry-flyai-verification.js'));
|
|
649
660
|
copyFileSync(join(repoRoot, 'bin', 'gotry-extension-status.js'), join(pkgBin, 'gotry-extension-status.js'));
|
|
661
|
+
copyFileSync(join(repoRoot, 'bin', 'gotry-hbcli-release.js'), join(pkgBin, 'gotry-hbcli-release.js'));
|
|
650
662
|
copyFileSync(join(repoRoot, 'bin', 'gotry-runtime-resolution.js'), join(pkgBin, 'gotry-runtime-resolution.js'));
|
|
651
663
|
copyFileSync(join(repoRoot, 'bin', 'gotry-process-liveness.js'), join(pkgBin, 'gotry-process-liveness.js'));
|
|
652
664
|
copyFileSync(join(repoRoot, 'cordis.gotry-patch.yml'), join(pkgRoot, 'cordis.gotry-patch.yml'));
|
|
@@ -1508,8 +1520,9 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1508
1520
|
assert.deepEqual(unit('not json'), { ok: true, customer: false }, '非 JSON(超旧版/异常)保守视为有效,与工具层同语义');
|
|
1509
1521
|
assert.deepEqual(unit(''), { ok: true, customer: false }, '空输出同属不可解析,保守视为有效');
|
|
1510
1522
|
assert.deepEqual(unit(null), { ok: false, customer: false }, '取不到输出(非零退出/超时/spawn 失败)→ 未确认');
|
|
1511
|
-
const fakeHbcli = (whoamiArm) => `#!/bin/sh\ncase "$*" in\n *--version*) echo '
|
|
1523
|
+
const fakeHbcli = (whoamiArm, version = '0.0.4') => `#!/bin/sh\ncase "$*" in\n *--version*) echo '${version}';;\n *version*) echo '${version}';;\n *whoami*) ${whoamiArm};;\nesac\n`;
|
|
1512
1524
|
const cases = [
|
|
1525
|
+
{ name: 'old-version', version: '0.0.3', arm: `echo '${allFalse.replace('"api_key":{"configured":false}', '"api_key":{"configured":true}')}'`, expect: 'missing' },
|
|
1513
1526
|
{ name: 'allfalse', arm: `echo '${allFalse}'`, expect: 'degraded' },
|
|
1514
1527
|
{ name: 'customer', arm: `echo '${allFalse.replace('"customer":{"configured":false}', '"customer":{"configured":true}')}'`, expect: 'ok', customer: true },
|
|
1515
1528
|
{ name: 'apikey', arm: `echo '${allFalse.replace('"api_key":{"configured":false}', '"api_key":{"configured":true}')}'`, expect: 'ok' },
|
|
@@ -1527,12 +1540,12 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1527
1540
|
mkdirSync(binDir);
|
|
1528
1541
|
mkdirSync(home);
|
|
1529
1542
|
symlinkSync(process.execPath, join(binDir, 'node'));
|
|
1530
|
-
writeFileSync(join(binDir, 'hbcli'), fakeHbcli(c.arm), { mode: 0o755 });
|
|
1543
|
+
writeFileSync(join(binDir, 'hbcli'), fakeHbcli(c.arm, c.version), { mode: 0o755 });
|
|
1531
1544
|
// CLI 面
|
|
1532
1545
|
const cli = runBootstrap(['doctor'], { HOME: home, PATH: binDir, FLYAI_API_KEY: '' });
|
|
1533
1546
|
const line = cli.out.split('\n').find((l) => l.includes('hbcli(酒店实时源)'));
|
|
1534
1547
|
assert.ok(line, `${c.name}: CLI doctor 应输出 hbcli 行\n${cli.out}`);
|
|
1535
|
-
const cliLevel = line.includes('已安装且凭证有效') ? 'ok' : line.includes('二进制在,但凭证未配置/失效') ? 'degraded' : 'other';
|
|
1548
|
+
const cliLevel = line.includes('已安装且凭证有效') ? 'ok' : line.includes('二进制在,但凭证未配置/失效') ? 'degraded' : line.includes('版本过旧') ? 'missing' : 'other';
|
|
1536
1549
|
assert.equal(cliLevel, c.expect, `${c.name}: CLI doctor 的 hbcli 结论应为 ${c.expect}\n${line}`);
|
|
1537
1550
|
if (c.customer)
|
|
1538
1551
|
assert.ok(line.includes('customer 档在用'), `${c.name}: CLI 应标注 customer 档在用\n${line}`);
|
|
@@ -1542,6 +1555,10 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1542
1555
|
process.env.PATH = prevPath;
|
|
1543
1556
|
assert.equal(tool.status, c.expect, `${c.name}: 工具层 doctor 的 hbcli 结论应为 ${c.expect},实际 ${tool.status}`);
|
|
1544
1557
|
assert.equal(cliLevel, tool.status, `${c.name}: CLI 与工具层必须同结论(两份实现不得漂移)`);
|
|
1558
|
+
if (c.expect === 'missing') {
|
|
1559
|
+
assert.match(cli.out, /staicli-v0\.0\.4\/install\.sh/, 'CLI 应提供当前官方版本的安装入口');
|
|
1560
|
+
assert.match(tool.fix ?? '', /staicli-v0\.0\.4\/install\.sh/, '工具层应提供相同升级入口');
|
|
1561
|
+
}
|
|
1545
1562
|
}
|
|
1546
1563
|
finally {
|
|
1547
1564
|
process.env.PATH = prevPath;
|
|
@@ -1560,5 +1577,5 @@ console.log('23. result 通道排他写入(预存文件不被覆盖 + symlink
|
|
|
1560
1577
|
catch { /* ignore */ }
|
|
1561
1578
|
}
|
|
1562
1579
|
}
|
|
1563
|
-
console.log('24. hbcli
|
|
1580
|
+
console.log('24. hbcli 版本与凭证判定(CLI↔工具层 6 夹具同结论:旧版=missing / 全 false=degraded / customer·api_key=ok / 非 JSON=保守 ok / 非零退出=degraded)OK');
|
|
1564
1581
|
console.log('BOOTSTRAP TESTS: 26/26 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / doctor 体检面 / calendar setup 状态面 / 显式跳过 + auto 跳过 + 单项跳过 / 启动摘要 / sidebar 落盘状态复核 / onboarding 分类+计划+env opt-out+跳过原因+yes+partial-failure+幂等+prompt+CLI 跳过+--scan / 编排缝 orchestrateWebLaunch × 6 / prompt waiter failure deterministic process-group reap / waiter 原始错误 + cleanup timeout/failure handled / 临时安装包 spawned inner 21a TTY-eligible 真实 prompt→n→web + 摘要抑制 + 无残留 / 21b non-TTY 零 prompt / 21c POSIX 信号清理 SIGTERM→清 result+patch 目录 + 无残留子进程 / 21f yes-path installer 子树信号清理 / 21g yes-path timeout installer 子树清理 / 21d timeout 诊断 + 无残留 / 21e dsh-lifecycle 信号转发 / win32 平台边界 / result 通道排他写入 / hbcli 凭证判定 CLI↔工具层对拍)');
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Supplier metadata fixtures; no vendor requests, credentials or browser session. */
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseCtripHotelCities, resolveCtripHotelCity, parseCtripCityCatalog, parseCtripCitySuggestions, matchCtripCity, resolveCtripFlightCity } from '../capabilities/session/ctrip-city.js';
|
|
4
|
+
import { resolveFlightEntryUrl } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
5
|
+
const catalog = { ResponseStatus: { Ack: 'Success' }, responseHead: { errorCode: '0' }, recommendGroupList: [{ indexedCity: { cityList: [
|
|
6
|
+
{ id: 2, code: 'SHA', name: '上海', eName: 'Shanghai', airportlst: [{ code: 'PVG', name: '上海浦东国际机场' }] },
|
|
7
|
+
{ id: 58, code: 'HKG', name: '中国香港', nameEn: 'Hong Kong' },
|
|
8
|
+
] }, geographicalRegionList: [{ hotCityList: [{ cityList: [{ id: 220, code: 'DXB', name: '迪拜', eName: 'Dubai', specName: '迪拜(阿联酋)', sameSpecCity: 1 }] }] }] }] };
|
|
9
|
+
const cities = parseCtripCityCatalog(catalog);
|
|
10
|
+
for (const [name, code] of [['迪拜(阿联酋)', 'DXB'], ['DXB', 'DXB'], ['香港', 'HKG'], ['Hong Kong', 'HKG'], ['Shanghai', 'SHA'], ['上海浦东国际机场', 'PVG']]) {
|
|
11
|
+
const r = matchCtripCity(name, cities);
|
|
12
|
+
assert.ok(r.ok, name);
|
|
13
|
+
assert.equal(r.city.code, code);
|
|
14
|
+
}
|
|
15
|
+
assert.equal(matchCtripCity('Dubai', cities).ok, false, 'supplier-marked same-name cities require qualification');
|
|
16
|
+
assert.equal(matchCtripCity('迪', cities).ok, false, 'no prefix or nearby-city substitution');
|
|
17
|
+
assert.equal(matchCtripCity('同名城', [{ name: '同名城', code: 'AAA', aliases: ['同名城'], context: '甲国' }, { name: '同名城', code: 'BBB', aliases: ['同名城'], context: '乙国' }]).ok, false);
|
|
18
|
+
assert.throws(() => parseCtripCityCatalog({ ...catalog, responseHead: { errorCode: '1' } }));
|
|
19
|
+
const suggestions = { Data: [{ Type: 4, Name: '未缓存城市', Code: 'NEW', EName: 'New City', Country: '示例国', Nearby: [{ Type: 4, Name: '邻近城市', Code: 'NEA' }] }] };
|
|
20
|
+
assert.equal(parseCtripCitySuggestions(suggestions).length, 1);
|
|
21
|
+
let requests = [];
|
|
22
|
+
const fakeFetch = async (url) => {
|
|
23
|
+
requests.push(String(url));
|
|
24
|
+
return Response.json(requests.length === 1 ? catalog : suggestions);
|
|
25
|
+
};
|
|
26
|
+
const resolved = await resolveCtripFlightCity('未缓存城市', fakeFetch);
|
|
27
|
+
assert.ok(resolved.ok);
|
|
28
|
+
assert.equal(resolved.city.code, 'NEW');
|
|
29
|
+
assert.equal(requests.length, 2, 'catalog miss queries supplier autocomplete');
|
|
30
|
+
const blocked = await resolveCtripFlightCity('未知城市', async () => new Response('whaleguard block'));
|
|
31
|
+
assert.ok(!blocked.ok && blocked.reason === 'unavailable', 'challenge is unavailable, not unsupported/no inventory');
|
|
32
|
+
const oversized = await resolveCtripFlightCity('未知城市', async () => new Response('x'.repeat(512001)));
|
|
33
|
+
assert.ok(!oversized.ok && oversized.reason === 'unavailable');
|
|
34
|
+
const entry = await resolveFlightEntryUrl('迪拜(阿联酋)', '香港', '2026-12-01', {}, async (q) => matchCtripCity(q, cities));
|
|
35
|
+
assert.ok(entry.ok && entry.url?.includes('oneway-dxb-hkg'));
|
|
36
|
+
let lookups = 0;
|
|
37
|
+
assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', 'bad-date', {}, async () => { lookups++; throw Error('should not run'); })).ok);
|
|
38
|
+
assert.ok(!(await resolveFlightEntryUrl('Dubai', '香港', '2026-12-01', { fromCode: '../dxb' }, async () => { lookups++; throw Error('should not run'); })).ok);
|
|
39
|
+
assert.equal(lookups, 0);
|
|
40
|
+
console.log('Ctrip city metadata: international names/airports, ambiguity, autocomplete fallback, challenge/bounds and exact URL PASS');
|
|
41
|
+
const hotelRecord = (id, name, english, country) => ({ keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: String(id), displayTexts: [{ key: 'MAIN_TITLE', value: english }] } }, controlInfo: { regionInfo: { basicCityModel: { cityId: id }, displayCityModel: { cityName: name, countryName: country, destinationNameEN: english } } } });
|
|
42
|
+
const hotels = { ResponseStatus: { Ack: 'Success' }, data: { mainKeywordList: { keywords: [hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), hotelRecord(596578, '迪拜', 'Dubai', '印度')] } } };
|
|
43
|
+
const hotelFetch = async () => Response.json(hotels);
|
|
44
|
+
const ambiguous = await resolveCtripHotelCity('迪拜', undefined, hotelFetch);
|
|
45
|
+
assert.ok(!ambiguous.ok && ambiguous.reason === 'ambiguous');
|
|
46
|
+
assert.equal(ambiguous.candidates.length, 2);
|
|
47
|
+
const qualified = await resolveCtripHotelCity('迪拜', '阿联酋', hotelFetch);
|
|
48
|
+
assert.ok(qualified.ok && qualified.city.id === '220');
|
|
49
|
+
const rejected = await resolveCtripHotelCity('迪拜', undefined, async () => Response.json({ ...hotels, data: { ...hotels.data, htlSpiderActionErrorCode: 'captcha' } }));
|
|
50
|
+
assert.ok(!rejected.ok && rejected.reason === 'unavailable');
|
|
51
|
+
assert.throws(() => parseCtripHotelCities({ ...hotels, data: { mainKeywordList: { keywords: [{ ...hotelRecord(220, '迪拜', 'Dubai', '阿联酋'), keyword: { keywordContentInfo: { tripType: 'CT', keywordCode: '58' } } }] } } }));
|
|
52
|
+
console.log('Hotel destination namespace: same-name city ambiguity, country confirmation, challenge and ID conflict PASS');
|
|
53
|
+
// Exercise the real registered schema and tool dispatch, using an isolated state root.
|
|
54
|
+
const { apply } = await import('../src/index.js');
|
|
55
|
+
const { mkdtempSync, rmSync } = await import('node:fs');
|
|
56
|
+
const { tmpdir } = await import('node:os');
|
|
57
|
+
const { join } = await import('node:path');
|
|
58
|
+
const root = mkdtempSync(join(tmpdir(), 'gotry-city-tool-'));
|
|
59
|
+
const tools = [];
|
|
60
|
+
const effects = [];
|
|
61
|
+
try {
|
|
62
|
+
apply({ tools: { register: (t) => tools.push(t) }, systemPrompt: { variable: () => { } }, get: () => undefined, on: () => () => { } }, { stateRoot: root, timeoutMs: 1000, hbcliBin: 'unused', sessionAccess: 'allow' }, { effect: (async (fx) => { effects.push(fx); return { result: { ok: false, verdict: 'error', error: 'fixture only', evidence: '[fixture:city-tool]' }, trace: { effect: fx.effect, channel: 'browser', attempts: 1, backoffMs: 0, breaker: 'off', evidence: [] } }; }) });
|
|
63
|
+
const session = tools.find(t => t.name === 'gotry_session_search');
|
|
64
|
+
const hotel = tools.find(t => t.name === 'gotry_hotel_search');
|
|
65
|
+
const parameters = session.parameters?.properties ?? session.parameters;
|
|
66
|
+
assert.ok(parameters.country && parameters.fromCityCode && parameters.toCityCode, 'model must see disambiguation parameters on session search');
|
|
67
|
+
const hotelParameters = hotel.parameters?.properties ?? hotel.parameters;
|
|
68
|
+
assert.ok(!hotelParameters.country, 'HBCLI hotel tool must not advertise an ignored session parameter');
|
|
69
|
+
await session.execute({ kind: 'hotel', to: '迪拜', country: '阿联酋' }, {});
|
|
70
|
+
assert.equal(effects.at(-1).effect, 'SESSION_HOTEL_SEARCH');
|
|
71
|
+
assert.equal(effects.at(-1).params.country, '阿联酋');
|
|
72
|
+
await session.execute({ kind: 'flight', from: '迪拜', to: '香港', date: '2099-12-01', fromCityCode: 'DXB', toCityCode: 'HKG' }, {});
|
|
73
|
+
assert.equal(effects.at(-1).effect, 'SESSION_FLIGHT_SEARCH');
|
|
74
|
+
assert.equal(effects.at(-1).params.fromCityCode, 'DXB');
|
|
75
|
+
assert.equal(effects.at(-1).params.toCityCode, 'HKG');
|
|
76
|
+
console.log('Registered session tool: country/code schema and effect dispatch PASS');
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
rmSync(root, { recursive: true, force: true });
|
|
80
|
+
}
|