1688-cli 0.1.40 → 0.1.42
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/AGENTS.md +112 -318
- package/ARCHITECTURE.md +106 -0
- package/CHANGELOG.md +107 -0
- package/README.md +100 -10
- package/dist/cli.js +165 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/cart-add.js +15 -11
- package/dist/commands/cart-add.js.map +1 -1
- package/dist/commands/cart-list.js +13 -11
- package/dist/commands/cart-list.js.map +1 -1
- package/dist/commands/compare.js +107 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/debug.js +101 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/doctor.js +107 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/image-search.js +2 -6
- package/dist/commands/image-search.js.map +1 -1
- package/dist/commands/inbox.js +16 -32
- package/dist/commands/inbox.js.map +1 -1
- package/dist/commands/offer.js +7 -5
- package/dist/commands/offer.js.map +1 -1
- package/dist/commands/order-list.js +4 -2
- package/dist/commands/order-list.js.map +1 -1
- package/dist/commands/order-logistics.js +4 -2
- package/dist/commands/order-logistics.js.map +1 -1
- package/dist/commands/profile.js +84 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/research.js +142 -0
- package/dist/commands/research.js.map +1 -0
- package/dist/commands/search.js +157 -111
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/seller-messages.js +7 -4
- package/dist/commands/seller-messages.js.map +1 -1
- package/dist/commands/similar.js +2 -6
- package/dist/commands/similar.js.map +1 -1
- package/dist/commands/sourcing-utils.js +438 -0
- package/dist/commands/sourcing-utils.js.map +1 -0
- package/dist/commands/supplier-inspect.js +559 -0
- package/dist/commands/supplier-inspect.js.map +1 -0
- package/dist/commands/supplier-search.js +522 -0
- package/dist/commands/supplier-search.js.map +1 -0
- package/dist/daemon/client.js +2 -2
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/protocol.js +2 -1
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/io/output.js +31 -2
- package/dist/io/output.js.map +1 -1
- package/dist/session/config.js +82 -0
- package/dist/session/config.js.map +1 -0
- package/dist/session/dispatch.js +26 -3
- package/dist/session/dispatch.js.map +1 -1
- package/dist/session/events.js +151 -0
- package/dist/session/events.js.map +1 -0
- package/dist/session/im-cards.js +183 -0
- package/dist/session/im-cards.js.map +1 -0
- package/dist/session/im-ws.js +8 -5
- package/dist/session/im-ws.js.map +1 -1
- package/dist/session/navigation-guard.js +65 -0
- package/dist/session/navigation-guard.js.map +1 -0
- package/dist/session/paths.js +23 -4
- package/dist/session/paths.js.map +1 -1
- package/dist/session/search-capture.js +56 -6
- package/dist/session/search-capture.js.map +1 -1
- package/dist/session/search-mtop.js +53 -0
- package/dist/session/search-mtop.js.map +1 -1
- package/dist/session/supplier-search.js +403 -0
- package/dist/session/supplier-search.js.map +1 -0
- package/dist/util/encoding.js +8 -0
- package/dist/util/encoding.js.map +1 -0
- package/dist/util/temp.js +6 -0
- package/dist/util/temp.js.map +1 -0
- package/docs/AGENT_MAPS_PLAN.md +171 -0
- package/docs/AGENT_WORKING_PRINCIPLES.md +143 -0
- package/docs/COMMANDS.md +199 -0
- package/docs/FEATURES.md +45 -0
- package/docs/JSON_CONTRACTS.md +390 -0
- package/docs/QUALITY_SCORE.md +60 -0
- package/docs/README.md +35 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/SAFETY.md +100 -0
- package/docs/WORKFLOW.md +82 -0
- package/docs/exec-plans/README.md +9 -0
- package/docs/exec-plans/active/README.md +4 -0
- package/docs/exec-plans/completed/2026-05-28-sourcing-research-v1.md +125 -0
- package/docs/exec-plans/completed/2026-05-31-supplier-inspect-v1.md +113 -0
- package/docs/exec-plans/completed/2026-06-04-supplier-search-v1.md +81 -0
- package/docs/exec-plans/completed/2026-06-07-windows-cli-compatibility.md +138 -0
- package/docs/exec-plans/completed/README.md +4 -0
- package/docs/exec-plans/tech-debt-tracker.md +5 -0
- package/docs/generated/command-index.md +54 -0
- package/docs/generated/json-shapes.md +111 -0
- package/docs/generated/module-map.md +13 -0
- package/docs/generated/test-index.md +34 -0
- package/docs/playbooks/add-command.md +15 -0
- package/docs/playbooks/add-mtop-capture.md +13 -0
- package/docs/playbooks/change-json-output.md +11 -0
- package/docs/playbooks/debug-risk-control.md +12 -0
- package/docs/playbooks/update-cli-release.md +11 -0
- package/docs/specs/checkout-and-orders.md +30 -0
- package/docs/specs/seller-im.md +28 -0
- package/docs/specs/sourcing-research.md +186 -0
- package/docs/specs/supplier-inspect.md +144 -0
- package/docs/specs/supplier-search.md +179 -0
- package/docs/specs/windows-cli-compatibility.md +123 -0
- package/package.json +12 -2
- package/scripts/check_agent_map.mjs +86 -0
- package/scripts/fix_bin_mode.mjs +18 -0
- package/scripts/generate_agent_context.mjs +253 -0
- package/scripts/postinstall.mjs +12 -4
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Supplier Search And Research
|
|
2
|
+
|
|
3
|
+
This spec defines supplier discovery that starts from 1688's company search,
|
|
4
|
+
not product-offer aggregation.
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Help a buyer or agent answer:
|
|
9
|
+
|
|
10
|
+
- Which suppliers match this category keyword?
|
|
11
|
+
- Which matching suppliers expose factory/trust/service signals?
|
|
12
|
+
- Which suppliers deserve deeper `supplier inspect` enrichment?
|
|
13
|
+
- Which suppliers should be contacted or compared after product discovery?
|
|
14
|
+
|
|
15
|
+
## Source Boundary
|
|
16
|
+
|
|
17
|
+
`supplier search` and `supplier research` must use 1688 company search. The
|
|
18
|
+
known durable business endpoint from live probing is:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
search.1688.com/service/companySearchBusinessService
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The page entry URL is:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
https://s.1688.com/company/company_search.htm?keywords=<GBK-percent-keyword>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Important encoding rule: `s.1688.com` expects GBK percent-encoded keywords.
|
|
31
|
+
UTF-8 percent-encoding can search for mojibake and return zero or irrelevant
|
|
32
|
+
results.
|
|
33
|
+
|
|
34
|
+
Do not implement supplier discovery by running offer search and grouping
|
|
35
|
+
offers by supplier. That is a different signal and can hide suppliers that are
|
|
36
|
+
available in company search.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
### `supplier search`
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
1688 supplier search <keyword...> \
|
|
44
|
+
--max 20 \
|
|
45
|
+
--factory-only \
|
|
46
|
+
--province 广东 \
|
|
47
|
+
--city 深圳 \
|
|
48
|
+
--min-years 3 \
|
|
49
|
+
--min-repeat-rate 0.4 \
|
|
50
|
+
--min-response-rate 0.6 \
|
|
51
|
+
--enrich 0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Default behavior is supplier discovery only. `--enrich` is optional and
|
|
55
|
+
defaults to `0`.
|
|
56
|
+
|
|
57
|
+
### `supplier research`
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
1688 supplier research <keyword...> \
|
|
61
|
+
--max 20 \
|
|
62
|
+
--factory-only \
|
|
63
|
+
--enrich top:10 \
|
|
64
|
+
--jsonl
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`supplier research` uses the same company-search source and scoring, but
|
|
68
|
+
defaults to `--enrich top:10`. Enrichment calls `supplier inspect` with the
|
|
69
|
+
company-search `memberId` when present.
|
|
70
|
+
|
|
71
|
+
Supported export modes:
|
|
72
|
+
|
|
73
|
+
- default: human table
|
|
74
|
+
- JSON: automatic when stdout is piped or `--json` is used
|
|
75
|
+
- `--jsonl`: one supplier item per line
|
|
76
|
+
- `--csv`: comma-separated table
|
|
77
|
+
- `--output <file>`: write JSONL/CSV to a file
|
|
78
|
+
|
|
79
|
+
## Data Model
|
|
80
|
+
|
|
81
|
+
Each item records source keyword/rank, normalized company-search supplier
|
|
82
|
+
signals, score, and optional inspect enrichment:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
{
|
|
86
|
+
sourceKeyword: string,
|
|
87
|
+
sourceRank: number,
|
|
88
|
+
globalRank: number,
|
|
89
|
+
supplier: {
|
|
90
|
+
companyName: string,
|
|
91
|
+
loginId: string | null,
|
|
92
|
+
memberId: string | null,
|
|
93
|
+
enterpriseId: string | null,
|
|
94
|
+
realUserId: string | null,
|
|
95
|
+
companyId: string | null,
|
|
96
|
+
shopUrl: string | null,
|
|
97
|
+
factoryCardUrl: string | null,
|
|
98
|
+
location: { province: string | null, city: string | null, address: string | null },
|
|
99
|
+
productionService: string | null,
|
|
100
|
+
tp: { serviceYears: number | null, memberLevel: string | null },
|
|
101
|
+
factory: {
|
|
102
|
+
isFactory: boolean,
|
|
103
|
+
factoryTag: string | null,
|
|
104
|
+
factoryLevel: string | null,
|
|
105
|
+
superFactory: boolean,
|
|
106
|
+
businessInspection: boolean,
|
|
107
|
+
factoryInspection: boolean,
|
|
108
|
+
},
|
|
109
|
+
service: {
|
|
110
|
+
compositeScore: number | null,
|
|
111
|
+
wwResponseRate: number | null,
|
|
112
|
+
repeatRate: number | null,
|
|
113
|
+
},
|
|
114
|
+
demand: {
|
|
115
|
+
payOrderCount3m: number | null,
|
|
116
|
+
payAmount3m: number | null,
|
|
117
|
+
fuzzyPayAmount3m: string | null,
|
|
118
|
+
saleQuantity3m: number | null,
|
|
119
|
+
},
|
|
120
|
+
tags: string[],
|
|
121
|
+
offersPreview: SupplierOfferPreview[],
|
|
122
|
+
},
|
|
123
|
+
score: number,
|
|
124
|
+
scoreBreakdown: Array<{ name: string, points: number, reason: string }>,
|
|
125
|
+
inspect?: SupplierInspectResult,
|
|
126
|
+
error?: { code: string, message: string },
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The top-level result includes:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
source: {
|
|
134
|
+
kind: "company-search",
|
|
135
|
+
endpoint: "companySearchBusinessService",
|
|
136
|
+
offerAggregation: false,
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Score V1
|
|
141
|
+
|
|
142
|
+
The supplier score is a ranking aid, not a truth claim.
|
|
143
|
+
|
|
144
|
+
- Company-search demand: up to 25 points from 3-month pay order count.
|
|
145
|
+
- Supplier tenure: up to 15 points from service years.
|
|
146
|
+
- Factory/trust: up to 20 points from factory/super-factory/inspection flags.
|
|
147
|
+
- Service rates: up to 15 points from repeat and Wangwang response rates.
|
|
148
|
+
- Composite score: up to 10 points.
|
|
149
|
+
- Offer preview depth: up to 10 points from company-search previews.
|
|
150
|
+
|
|
151
|
+
## Failure Semantics
|
|
152
|
+
|
|
153
|
+
- Run-level failure: login expired, risk control, browser/network failure that
|
|
154
|
+
prevents company search from loading.
|
|
155
|
+
- Empty result: company search loads but returns no supplier payload.
|
|
156
|
+
- Item-level enrichment failure: `supplier inspect` fails for one supplier;
|
|
157
|
+
keep the supplier item and attach `error`.
|
|
158
|
+
|
|
159
|
+
If a command exits with risk-control code `4`, retry once with `--headed` and
|
|
160
|
+
solve the slider manually.
|
|
161
|
+
|
|
162
|
+
## V1 Boundaries
|
|
163
|
+
|
|
164
|
+
Live probing on 2026-06-04 showed the company search page emits
|
|
165
|
+
`companySearchBusinessService` with `companyWithOfferLists`. A typical first
|
|
166
|
+
page async response used `startIndex=6&asyncCount=14`; this likely means some
|
|
167
|
+
top-page suppliers may be server-rendered before the async service response.
|
|
168
|
+
V1 uses the stable browser-emitted business response and keeps the largest
|
|
169
|
+
captured company-search payload. A later V2 can add HTML/DOM extraction for
|
|
170
|
+
server-rendered supplier cards if we need exact 20-per-page completeness.
|
|
171
|
+
|
|
172
|
+
## Verification
|
|
173
|
+
|
|
174
|
+
- Unit tests cover GBK company-search URL construction.
|
|
175
|
+
- Unit tests cover `companySearchBusinessService` parsing and offer previews.
|
|
176
|
+
- Unit tests cover capture `keep: "largest"` behavior.
|
|
177
|
+
- Unit tests cover enrich option parsing and CSV escaping.
|
|
178
|
+
- `pnpm agent-context` refreshes generated command and JSON-shape indexes.
|
|
179
|
+
- `pnpm agent-verify` is the default gate.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Windows CLI Compatibility
|
|
2
|
+
|
|
3
|
+
This spec defines the compatibility baseline required before `1688-cli` can
|
|
4
|
+
claim normal Windows command-line support.
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Make the installed CLI and local development workflow work from Windows
|
|
9
|
+
PowerShell and cmd.exe for read-only and daemon-backed commands.
|
|
10
|
+
|
|
11
|
+
## Scope
|
|
12
|
+
|
|
13
|
+
- npm package install and build scripts must not depend on Unix shell commands.
|
|
14
|
+
- The daemon IPC path must work on Windows named pipes and avoid collisions
|
|
15
|
+
between different `BB1688_HOME` roots.
|
|
16
|
+
- Runtime paths, temporary files, and diagnostics must use Node path helpers
|
|
17
|
+
instead of hard-coded Unix paths where they are part of normal command code.
|
|
18
|
+
- `doctor` fix hints must be executable or meaningful on Windows.
|
|
19
|
+
- README and command docs must show Windows alternatives where Unix examples
|
|
20
|
+
use `jq`, shell assignment, `/tmp`, or `~/.1688`.
|
|
21
|
+
- Deterministic tests must cover the Windows-specific path and hint logic.
|
|
22
|
+
- CI/package verification must include a Windows-compatible build and smoke
|
|
23
|
+
path, or document the exact manual Windows checks when CI is not available
|
|
24
|
+
in the current environment.
|
|
25
|
+
|
|
26
|
+
## Non-Goals
|
|
27
|
+
|
|
28
|
+
- Do not bypass 1688 login, slider verification, or risk control.
|
|
29
|
+
- Do not automate Windows UI interaction beyond Playwright's existing browser
|
|
30
|
+
launch behavior.
|
|
31
|
+
- Do not guarantee live 1688 network/search success in CI; CI checks should be
|
|
32
|
+
deterministic and use `doctor --no-launch` unless a real account/session is
|
|
33
|
+
explicitly supplied.
|
|
34
|
+
- Do not change public JSON contracts except by additive diagnostic fields.
|
|
35
|
+
- Do not add a new installer, native binary, or Windows service wrapper.
|
|
36
|
+
|
|
37
|
+
## Behavior Contract
|
|
38
|
+
|
|
39
|
+
### Build And Install
|
|
40
|
+
|
|
41
|
+
`pnpm build` must run on Windows, macOS, and Linux. Any executable-bit fix must
|
|
42
|
+
be implemented in Node and become a no-op on Windows.
|
|
43
|
+
|
|
44
|
+
`scripts/postinstall.mjs` must:
|
|
45
|
+
|
|
46
|
+
- locate the daemon pid file under `BB1688_HOME` when set
|
|
47
|
+
- detect Windows Chrome install paths
|
|
48
|
+
- invoke `npx.cmd` on Windows and `npx` elsewhere
|
|
49
|
+
- print retry commands that are valid for the current platform
|
|
50
|
+
|
|
51
|
+
### Daemon IPC
|
|
52
|
+
|
|
53
|
+
Unix-like platforms continue to use `<BB1688_HOME>/daemon.sock`.
|
|
54
|
+
|
|
55
|
+
Windows must use a named pipe:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
\\.\pipe\1688-cli-daemon-<stable-root-hash>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The hash must be stable for a given `BB1688_HOME`/default root and different
|
|
62
|
+
for different roots so tests, profiles, and concurrent users do not collide.
|
|
63
|
+
|
|
64
|
+
### Diagnostics
|
|
65
|
+
|
|
66
|
+
`1688 doctor` must emit platform-appropriate fix hints:
|
|
67
|
+
|
|
68
|
+
- Unix-like stale lock: `rm -rf "..."`
|
|
69
|
+
- Windows stale lock: `Remove-Item -Recurse -Force "..."`
|
|
70
|
+
- Unix-like writable-directory issue: `chmod u+w "..."`
|
|
71
|
+
- Windows writable-directory issue: explain to grant write permission or choose
|
|
72
|
+
another `BB1688_HOME`
|
|
73
|
+
|
|
74
|
+
The daemon protocol documentation and README must not describe the daemon as
|
|
75
|
+
Unix-only.
|
|
76
|
+
|
|
77
|
+
### Documentation
|
|
78
|
+
|
|
79
|
+
README and command docs must include:
|
|
80
|
+
|
|
81
|
+
- PowerShell examples using built-in `--get`/`--pick` instead of requiring `jq`
|
|
82
|
+
- Windows output paths such as `$env:TEMP\suppliers.csv`
|
|
83
|
+
- Windows local state paths using `%USERPROFILE%\.1688` or `$env:USERPROFILE`
|
|
84
|
+
- named pipe note for daemon IPC on Windows
|
|
85
|
+
|
|
86
|
+
## Acceptance Criteria
|
|
87
|
+
|
|
88
|
+
- `pnpm build` succeeds without requiring `chmod` from the shell.
|
|
89
|
+
- Unit tests cover Windows named pipe generation and platform-specific doctor
|
|
90
|
+
hints.
|
|
91
|
+
- `pnpm test:unit` passes.
|
|
92
|
+
- `pnpm agent-verify` passes.
|
|
93
|
+
- `npm pack --dry-run` succeeds.
|
|
94
|
+
- README and `docs/COMMANDS.md` no longer present Unix-only examples as the
|
|
95
|
+
only way to use JSON, output files, or local paths.
|
|
96
|
+
- Manual Windows smoke checklist is documented:
|
|
97
|
+
- `npm i -g 1688-cli`
|
|
98
|
+
- `1688 --version`
|
|
99
|
+
- `1688 doctor --no-launch --json`
|
|
100
|
+
- `1688 daemon start`
|
|
101
|
+
- `1688 daemon status --json`
|
|
102
|
+
- `1688 daemon stop`
|
|
103
|
+
- `1688 search 雨伞 --max 1 --json`
|
|
104
|
+
- `1688 supplier search 键盘 --max 1 --json`
|
|
105
|
+
|
|
106
|
+
## Verification Signals
|
|
107
|
+
|
|
108
|
+
- Focused tests:
|
|
109
|
+
- `tests/paths.test.ts`
|
|
110
|
+
- `tests/doctor.test.ts` or existing doctor tests
|
|
111
|
+
- Package checks:
|
|
112
|
+
- `pnpm build`
|
|
113
|
+
- `pnpm test:unit`
|
|
114
|
+
- `pnpm agent-verify`
|
|
115
|
+
- `npm pack --dry-run`
|
|
116
|
+
- Manual Windows smoke checks listed above when a Windows machine/session is
|
|
117
|
+
available.
|
|
118
|
+
|
|
119
|
+
## Open Questions
|
|
120
|
+
|
|
121
|
+
- Whether to add GitHub Actions `windows-latest` is a repository operations
|
|
122
|
+
decision. The code/docs work should make that job straightforward, but adding
|
|
123
|
+
CI config is not required unless the repository already uses GitHub Actions.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "1688-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "1688.com CLI for humans, Codex, and Claude Code. Sourcing (search / image-search / offer / inquire) and orders (list / detail / logistics / seller chat).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "nobodyjack",
|
|
@@ -36,7 +36,12 @@
|
|
|
36
36
|
"files": [
|
|
37
37
|
"dist",
|
|
38
38
|
"scripts/postinstall.mjs",
|
|
39
|
+
"scripts/fix_bin_mode.mjs",
|
|
40
|
+
"scripts/generate_agent_context.mjs",
|
|
41
|
+
"scripts/check_agent_map.mjs",
|
|
42
|
+
"docs",
|
|
39
43
|
"AGENTS.md",
|
|
44
|
+
"ARCHITECTURE.md",
|
|
40
45
|
"README.md",
|
|
41
46
|
"LICENSE",
|
|
42
47
|
"CHANGELOG.md"
|
|
@@ -46,10 +51,15 @@
|
|
|
46
51
|
},
|
|
47
52
|
"scripts": {
|
|
48
53
|
"dev": "tsx src/cli.ts",
|
|
49
|
-
"build": "tsc -p tsconfig.json &&
|
|
54
|
+
"build": "tsc -p tsconfig.json && node scripts/fix_bin_mode.mjs",
|
|
50
55
|
"test": "vitest run",
|
|
56
|
+
"test:unit": "vitest run --exclude tests/doctor-live.test.ts",
|
|
51
57
|
"test:watch": "vitest",
|
|
52
58
|
"typecheck": "tsc --noEmit",
|
|
59
|
+
"agent-context": "node scripts/generate_agent_context.mjs",
|
|
60
|
+
"docs-check": "node scripts/generate_agent_context.mjs --check",
|
|
61
|
+
"agent-map-check": "node scripts/check_agent_map.mjs",
|
|
62
|
+
"agent-verify": "pnpm typecheck && pnpm test:unit && pnpm docs-check && pnpm agent-map-check",
|
|
53
63
|
"prepublishOnly": "pnpm build",
|
|
54
64
|
"postinstall": "node scripts/postinstall.mjs"
|
|
55
65
|
},
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
|
|
8
|
+
async function read(relPath) {
|
|
9
|
+
return fs.readFile(path.join(root, relPath), 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function exists(relPath) {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(path.join(root, relPath));
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const requiredFiles = [
|
|
22
|
+
'AGENTS.md',
|
|
23
|
+
'ARCHITECTURE.md',
|
|
24
|
+
'docs/AGENT_WORKING_PRINCIPLES.md',
|
|
25
|
+
'docs/README.md',
|
|
26
|
+
'docs/WORKFLOW.md',
|
|
27
|
+
'docs/COMMANDS.md',
|
|
28
|
+
'docs/JSON_CONTRACTS.md',
|
|
29
|
+
'docs/SAFETY.md',
|
|
30
|
+
'docs/RELIABILITY.md',
|
|
31
|
+
'docs/QUALITY_SCORE.md',
|
|
32
|
+
'docs/FEATURES.md',
|
|
33
|
+
'docs/specs/sourcing-research.md',
|
|
34
|
+
'docs/specs/seller-im.md',
|
|
35
|
+
'docs/specs/checkout-and-orders.md',
|
|
36
|
+
'docs/playbooks/add-command.md',
|
|
37
|
+
'docs/playbooks/change-json-output.md',
|
|
38
|
+
'docs/playbooks/debug-risk-control.md',
|
|
39
|
+
'docs/playbooks/add-mtop-capture.md',
|
|
40
|
+
'docs/playbooks/update-cli-release.md',
|
|
41
|
+
'docs/generated/command-index.md',
|
|
42
|
+
'docs/generated/module-map.md',
|
|
43
|
+
'docs/generated/test-index.md',
|
|
44
|
+
'docs/generated/json-shapes.md',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const missing = [];
|
|
48
|
+
for (const file of requiredFiles) {
|
|
49
|
+
if (!(await exists(file))) missing.push(file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const failures = [];
|
|
53
|
+
if (missing.length) failures.push(`Missing files: ${missing.join(', ')}`);
|
|
54
|
+
|
|
55
|
+
const agents = await read('AGENTS.md').catch(() => '');
|
|
56
|
+
if (!agents.includes('pnpm agent-verify'))
|
|
57
|
+
failures.push('AGENTS.md must mention pnpm agent-verify.');
|
|
58
|
+
if (!agents.includes('docs/playbooks'))
|
|
59
|
+
failures.push('AGENTS.md must route work to docs/playbooks.');
|
|
60
|
+
if (!agents.includes('docs/AGENT_WORKING_PRINCIPLES.md'))
|
|
61
|
+
failures.push('AGENTS.md must link docs/AGENT_WORKING_PRINCIPLES.md.');
|
|
62
|
+
if (agents.split('\n').length > 220)
|
|
63
|
+
failures.push('AGENTS.md should stay short (<= 220 lines).');
|
|
64
|
+
|
|
65
|
+
const pkg = JSON.parse(await read('package.json'));
|
|
66
|
+
for (const scriptName of ['agent-context', 'docs-check', 'agent-map-check', 'agent-verify']) {
|
|
67
|
+
if (!pkg.scripts?.[scriptName]) failures.push(`package.json missing script: ${scriptName}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const docsReadme = await read('docs/README.md').catch(() => '');
|
|
71
|
+
for (const needle of [
|
|
72
|
+
'AGENT_WORKING_PRINCIPLES.md',
|
|
73
|
+
'COMMANDS.md',
|
|
74
|
+
'JSON_CONTRACTS.md',
|
|
75
|
+
'SAFETY.md',
|
|
76
|
+
'generated/',
|
|
77
|
+
]) {
|
|
78
|
+
if (!docsReadme.includes(needle)) failures.push(`docs/README.md must link ${needle}.`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (failures.length) {
|
|
82
|
+
console.error(failures.join('\n'));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('Agent map structure looks good.');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export async function fixBinMode({
|
|
6
|
+
platform = process.platform,
|
|
7
|
+
target = path.join('dist', 'cli.js'),
|
|
8
|
+
} = {}) {
|
|
9
|
+
if (platform === 'win32') {
|
|
10
|
+
return { changed: false, reason: 'windows-noop', target };
|
|
11
|
+
}
|
|
12
|
+
await fs.chmod(target, 0o755);
|
|
13
|
+
return { changed: true, reason: 'chmod-755', target };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
17
|
+
await fixBinMode();
|
|
18
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const outDir = path.join(root, 'docs', 'generated');
|
|
8
|
+
const checkOnly = process.argv.includes('--check');
|
|
9
|
+
|
|
10
|
+
const rel = (p) => path.relative(root, p).replaceAll(path.sep, '/');
|
|
11
|
+
const read = (p) => fs.readFile(path.join(root, p), 'utf8');
|
|
12
|
+
|
|
13
|
+
async function exists(p) {
|
|
14
|
+
try {
|
|
15
|
+
await fs.access(path.join(root, p));
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function walk(dir, predicate = () => true) {
|
|
23
|
+
const abs = path.join(root, dir);
|
|
24
|
+
const out = [];
|
|
25
|
+
async function visit(current) {
|
|
26
|
+
const entries = await fs.readdir(current, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
29
|
+
const p = path.join(current, entry.name);
|
|
30
|
+
if (entry.isDirectory()) {
|
|
31
|
+
await visit(p);
|
|
32
|
+
} else if (predicate(p)) {
|
|
33
|
+
out.push(p);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (await exists(dir)) await visit(abs);
|
|
38
|
+
return out.sort((a, b) => rel(a).localeCompare(rel(b)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function header(title) {
|
|
42
|
+
return `# ${title}\n\n_Generated by \`scripts/generate_agent_context.mjs\`._\n\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function cleanDescription(raw) {
|
|
46
|
+
if (!raw) return '-';
|
|
47
|
+
return raw
|
|
48
|
+
.replace(/[`'"]/g, '')
|
|
49
|
+
.replace(/\s*\+\s*/g, ' ')
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim()
|
|
52
|
+
.replace(/\|/g, '\\|') || '-';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compactList(items) {
|
|
56
|
+
return items.length ? items.map((x) => `\`${x}\``).join('<br>') : '-';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function commandIndex() {
|
|
60
|
+
const cli = await read('src/cli.ts');
|
|
61
|
+
const re =
|
|
62
|
+
/(?:(?:const\s+([A-Za-z0-9_]+)\s*=\s*)?([A-Za-z0-9_]+)\s*\n\s*\.command\('([^']+)'\))/g;
|
|
63
|
+
const matches = [...cli.matchAll(re)];
|
|
64
|
+
const varToCommand = new Map();
|
|
65
|
+
const rows = [];
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < matches.length; i++) {
|
|
68
|
+
const m = matches[i];
|
|
69
|
+
const assignedVar = m[1];
|
|
70
|
+
const receiver = m[2];
|
|
71
|
+
const command = m[3];
|
|
72
|
+
const start = m.index ?? 0;
|
|
73
|
+
const end = matches[i + 1]?.index ?? cli.length;
|
|
74
|
+
const block = cli.slice(start, end);
|
|
75
|
+
const parent = receiver === 'program' ? '' : varToCommand.get(receiver) ?? receiver;
|
|
76
|
+
const full = parent ? `${parent} ${command}` : command;
|
|
77
|
+
if (assignedVar) varToCommand.set(assignedVar, full);
|
|
78
|
+
|
|
79
|
+
const descRaw = block.match(/\.description\(([\s\S]*?)\)\s*(?:\.|\n)/)?.[1];
|
|
80
|
+
const args = [...block.matchAll(/\.argument\(\s*(['"`])([^'"`]+)\1/g)].map(
|
|
81
|
+
(x) => x[2],
|
|
82
|
+
);
|
|
83
|
+
const opts = [
|
|
84
|
+
...block.matchAll(/\.(?:requiredOption|option)\(\s*(['"`])([^'"`]+)\1/g),
|
|
85
|
+
].map((x) => x[2]);
|
|
86
|
+
const source = block.match(/import\('\.\/commands\/([^']+)\.js'\)/)?.[1];
|
|
87
|
+
rows.push({
|
|
88
|
+
command: full,
|
|
89
|
+
source: source ? `src/commands/${source}.ts` : '-',
|
|
90
|
+
args,
|
|
91
|
+
opts,
|
|
92
|
+
description: cleanDescription(descRaw),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let out = header('Command Index');
|
|
97
|
+
out += '| Command | Source | Arguments | Options | Description |\n';
|
|
98
|
+
out += '|---|---|---|---|---|\n';
|
|
99
|
+
for (const row of rows) {
|
|
100
|
+
out += `| \`${row.command}\` | ${row.source === '-' ? '-' : `\`${row.source}\``} | ${compactList(
|
|
101
|
+
row.args,
|
|
102
|
+
)} | ${compactList(row.opts)} | ${row.description} |\n`;
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function moduleMap() {
|
|
108
|
+
const files = await walk('src', (p) => p.endsWith('.ts'));
|
|
109
|
+
const tests = await walk('tests', (p) => p.endsWith('.test.ts') || p.endsWith('.spec.ts'));
|
|
110
|
+
const dirs = new Map();
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
const r = rel(file);
|
|
113
|
+
const parts = r.split('/');
|
|
114
|
+
const dir = parts.length > 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
115
|
+
const entry = dirs.get(dir) ?? { files: 0, tests: 0, notes: '-' };
|
|
116
|
+
entry.files++;
|
|
117
|
+
dirs.set(dir, entry);
|
|
118
|
+
}
|
|
119
|
+
for (const file of tests) {
|
|
120
|
+
const content = await fs.readFile(file, 'utf8');
|
|
121
|
+
const target = content.match(/from ['"]\.\.\/src\/([^/'"]+)/)?.[1];
|
|
122
|
+
if (target) {
|
|
123
|
+
const key = `src/${target}`;
|
|
124
|
+
const entry = dirs.get(key) ?? { files: 0, tests: 0, notes: '-' };
|
|
125
|
+
entry.tests++;
|
|
126
|
+
dirs.set(key, entry);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const notes = {
|
|
131
|
+
'src/commands': 'command executors and renderers',
|
|
132
|
+
'src/session': 'browser/session automation helpers',
|
|
133
|
+
'src/daemon': 'background daemon client/server/protocol',
|
|
134
|
+
'src/io': 'output, prompts, and errors',
|
|
135
|
+
'src/auth': 'login/session/cookie helpers',
|
|
136
|
+
'src/util': 'shared utilities',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
let out = header('Module Map');
|
|
140
|
+
out += '| Directory | Source Files | Related Tests | Notes |\n';
|
|
141
|
+
out += '|---|---:|---:|---|\n';
|
|
142
|
+
for (const dir of [...dirs.keys()].sort()) {
|
|
143
|
+
const entry = dirs.get(dir);
|
|
144
|
+
out += `| \`${dir}\` | ${entry.files} | ${entry.tests} | ${notes[dir] ?? entry.notes} |\n`;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function testIndex() {
|
|
150
|
+
const tests = await walk('tests', (p) => p.endsWith('.test.ts') || p.endsWith('.spec.ts'));
|
|
151
|
+
let out = header('Test Index');
|
|
152
|
+
out += '| Test File | Focus | Risk Notes |\n';
|
|
153
|
+
out += '|---|---|---|\n';
|
|
154
|
+
for (const file of tests) {
|
|
155
|
+
const content = await fs.readFile(file, 'utf8');
|
|
156
|
+
const name = path.basename(file).replace(/\.(test|spec)\.ts$/, '');
|
|
157
|
+
const risks = [];
|
|
158
|
+
if (/live|doctor-live|process\.env|BB1688|1688\s/.test(content + name))
|
|
159
|
+
risks.push('live/session-sensitive');
|
|
160
|
+
if (/playwright|BrowserContext|Page|page-state|risk-control/i.test(content))
|
|
161
|
+
risks.push('browser/page-state');
|
|
162
|
+
if (/fixture|jsonp|mtop|capture/i.test(content)) risks.push('fixture/parser');
|
|
163
|
+
out += `| \`${rel(file)}\` | ${name} | ${risks.join(', ') || '-'} |\n`;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractInterfaces(source, file) {
|
|
169
|
+
const lines = source.split('\n');
|
|
170
|
+
const interfaces = [];
|
|
171
|
+
for (let i = 0; i < lines.length; i++) {
|
|
172
|
+
const m = lines[i].match(/^export interface ([A-Za-z0-9_]+)/);
|
|
173
|
+
if (!m) continue;
|
|
174
|
+
const name = m[1];
|
|
175
|
+
const keep =
|
|
176
|
+
/(Args|Opts|Result|Offer|Message|CartItem|Order|Sku|Price|Attribute|Package)/.test(
|
|
177
|
+
name,
|
|
178
|
+
);
|
|
179
|
+
if (!keep) continue;
|
|
180
|
+
|
|
181
|
+
const body = [];
|
|
182
|
+
let depth = 0;
|
|
183
|
+
for (let j = i; j < lines.length; j++) {
|
|
184
|
+
const line = lines[j];
|
|
185
|
+
if (j > i) body.push(line);
|
|
186
|
+
depth += (line.match(/\{/g) ?? []).length;
|
|
187
|
+
depth -= (line.match(/\}/g) ?? []).length;
|
|
188
|
+
if (j > i && depth <= 0) break;
|
|
189
|
+
}
|
|
190
|
+
const fields = body
|
|
191
|
+
.map((line) => line.trim())
|
|
192
|
+
.filter((line) => /^[A-Za-z0-9_?]+:/.test(line) || /^\/\*\*/.test(line))
|
|
193
|
+
.slice(0, 14)
|
|
194
|
+
.map((line) => line.replace(/\|/g, '\\|'));
|
|
195
|
+
interfaces.push({ name, file, fields });
|
|
196
|
+
}
|
|
197
|
+
return interfaces;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function jsonShapes() {
|
|
201
|
+
const files = await walk('src', (p) => p.endsWith('.ts'));
|
|
202
|
+
const interfaces = [];
|
|
203
|
+
for (const file of files) {
|
|
204
|
+
const source = await fs.readFile(file, 'utf8');
|
|
205
|
+
interfaces.push(...extractInterfaces(source, rel(file)));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let out = header('JSON Shapes');
|
|
209
|
+
out +=
|
|
210
|
+
'This is a heuristic index of exported TypeScript interfaces that are likely to matter for agent-facing JSON.\n\n';
|
|
211
|
+
out += '| Interface | File | Notable Fields |\n';
|
|
212
|
+
out += '|---|---|---|\n';
|
|
213
|
+
for (const it of interfaces.sort((a, b) => a.file.localeCompare(b.file) || a.name.localeCompare(b.name))) {
|
|
214
|
+
out += `| \`${it.name}\` | \`${it.file}\` | ${it.fields.length ? it.fields.map((x) => `\`${x}\``).join('<br>') : '-'} |\n`;
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const outputs = new Map([
|
|
220
|
+
['command-index.md', await commandIndex()],
|
|
221
|
+
['module-map.md', await moduleMap()],
|
|
222
|
+
['test-index.md', await testIndex()],
|
|
223
|
+
['json-shapes.md', await jsonShapes()],
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
if (checkOnly) {
|
|
227
|
+
const mismatches = [];
|
|
228
|
+
for (const [name, content] of outputs) {
|
|
229
|
+
const target = path.join(outDir, name);
|
|
230
|
+
let current = '';
|
|
231
|
+
try {
|
|
232
|
+
current = await fs.readFile(target, 'utf8');
|
|
233
|
+
} catch {
|
|
234
|
+
mismatches.push(name);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (current !== content) mismatches.push(name);
|
|
238
|
+
}
|
|
239
|
+
if (mismatches.length) {
|
|
240
|
+
console.error(
|
|
241
|
+
`Generated agent context is stale: ${mismatches.join(', ')}. Run pnpm agent-context.`,
|
|
242
|
+
);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
console.log('Generated agent context is fresh.');
|
|
246
|
+
} else {
|
|
247
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
248
|
+
for (const [name, content] of outputs) {
|
|
249
|
+
await fs.writeFile(path.join(outDir, name), content);
|
|
250
|
+
}
|
|
251
|
+
console.log(`Generated agent context files in ${rel(outDir)}`);
|
|
252
|
+
}
|
|
253
|
+
|