Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit a5791f8

Browse files
committed
feat: impl fetch update_json
1 parent ad93a3c commit a5791f8

30 files changed

Lines changed: 1113 additions & 347 deletions

.github/copilot-instructions.md

Lines changed: 108 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,56 @@
1414

1515
### Plugin Data Model
1616
- **Plugin ID** (e.g., `[email protected]`): Unique identifier, used as directory name
17-
- **meta.json**: Manual metadata containing `id`, `name`, `update_json` URL, `description`, `homepage`, `tags`
17+
- **meta.json**: Manual metadata containing `id`, `name`, `updateUrl` (or deprecated `update_json`), `description`, `homepage`, `tags`, optional `patchedVersions`
1818
- **update.json**: Hosted by plugin developer, follows Zotero extension manifest format with version/compatibility data
1919
- **Tags**: Predefined enum (`metadata`, `interface`, `attachment`, `notes`, `reader`, `productivity`, `visualization`, `integration`, `ai`, `writing`, `developer`, `favorite`, `others`)
20+
- **patchedVersions**: Manual version list in meta.json for patching or supplementing remote versions
2021
- See [shared/src/types.ts](shared/src/types.ts) for complete type definitions
2122

2223
### Bot Processing Pipeline
23-
1. **CLI Entry** ([bot/src/cli.ts](bot/src/cli.ts)): Accepts optional plugin ID, defaults to all plugins
24-
2. **Process Plugins** ([bot/src/processor.ts](bot/src/processor.ts)): Reads `meta.json`, fetches remote `update.json`
25-
3. **Parse Versions**: Extracts version data from `addons[pluginId].updates` array in update.json
26-
4. **Extract Compatibility**: Maps `applications.zotero` and `applications.gecko` min/max versions
27-
5. **Cache**: Stores hash in `.cache.json` to detect update.json changes
28-
6. **Report**: ([bot/src/report.ts](bot/src/report.ts)) Handles GitHub integration and error reporting
29-
30-
### Authentication & External Access
31-
- **GitHub Token**: Required environment variable `GITHUB_TOKEN` for API-rate-limited GitHub requests
32-
- **HTTP Fetch** ([bot/src/utils.ts](bot/src/utils.ts)):
33-
- Detects GitHub URLs and adds Bearer token
34-
- Used for fetching remote `update.json` files
35-
- 10-second timeout for all requests
36-
- Axios-based with response type support (json, arraybuffer, etc.)
24+
1. **CLI Entry** ([bot/src/cli.ts](bot/src/cli.ts)): `zbot build` (default) or `zbot check` (PR validation)
25+
2. **Load Meta** ([bot/src/loaders/meta.ts](bot/src/loaders/meta.ts)): Validates `meta.json` schema and required fields
26+
3. **Fetch Remote** ([bot/src/loaders/update-json.ts](bot/src/loaders/update-json.ts)): Fetches remote `update.json` and parses `addons[pluginId].updates` array
27+
4. **Merge Versions** ([bot/src/merger.ts](bot/src/merger.ts)): Combines remote versions with `patchedVersions`, prioritizing patches, then sorts semantically
28+
5. **Extract Compatibility** ([bot/src/merger.ts](bot/src/merger.ts)): Determines min/max Zotero versions from version list
29+
6. **Generate Outputs** ([bot/src/processor.ts](bot/src/processor.ts)): Creates `meta.generated.json` and `latest.json`
30+
7. **Report Results** ([bot/src/report.ts](bot/src/report.ts)): Logs to console or GitHub (PR comments, issues)
31+
32+
### Authentication & HTTP
33+
- **GitHub Token**: `GITHUB_TOKEN` env var for GitHub API rate limits (Bearer auth on GitHub URLs)
34+
- **HTTP Fetch** ([bot/src/utils/http.ts](bot/src/utils/http.ts)): Axios with auto-detection of GitHub domains
35+
- **Timeout**: 10 seconds for fetch, 30 seconds for XPI download
36+
- **XPI Handling** ([bot/src/utils/xpi.ts](bot/src/utils/xpi.ts)): Download and validate XPI structure (manifest.json)
37+
38+
## Directory Structure (Bot)
39+
40+
```
41+
bot/src/
42+
├── cli.ts # Commander CLI entry point
43+
├── build.ts # Build orchestration (buildPlugins, checkPlugins)
44+
├── processor.ts # Single plugin processing pipeline
45+
├── merger.ts # Version merging and compatibility logic
46+
├── report.ts # Console/GitHub reporting
47+
├── github-reporter.ts # Octokit integration for PRs and issues
48+
├── cache.ts # (stub) Cache management
49+
├── loaders/
50+
│ ├── index.ts # Re-exports
51+
│ ├── meta.ts # loadPluginMeta() with validation
52+
│ └── update-json.ts # loadUpdateJson() with parsing
53+
└── utils/
54+
├── index.ts # Re-exports
55+
├── http.ts # fetchData() with GitHub auth
56+
├── xpi.ts # downloadXpi, extractXpiInfo, verifyXpi
57+
└── git.ts # detectChangedPlugins() for PR mode
58+
```
59+
60+
## Key Interfaces
61+
62+
**PluginMeta** (from shared): Core metadata with optional `patchedVersions` array
63+
**Version**: `{ version, update_link, update_hash?, strict_min_version?, strict_max_version? }`
64+
**GeneratedMeta**: Extended PluginMeta with merged versions, compatibility info, and stats
65+
**ProcessResult**: `{ success: string[], errors: PluginError[] }`
66+
**PluginError**: `{ pluginId, stage, message }` where stage is 'schema'|'fetch'|'xpi'|'merge'|'validation'
3767

3868
## Developer Workflows
3969

@@ -45,63 +75,89 @@ pnpm install
4575
# Process all plugins
4676
pnpm run build
4777

48-
# Process specific plugin by ID
49-
pnpm run --only [email protected]
78+
# Process specific plugin(s)
79+
pnpm run build plugin-id-1 plugin-id-2
80+
81+
# Check mode for PR validation
82+
pnpm run check [changed-plugin-ids]
5083

5184
# Generate TypeScript schema from types
5285
pnpm run -C shared generate-schema
86+
87+
# Linting
88+
pnpm run lint:fix
5389
```
5490

5591
### Adding New Plugins
5692
1. Create `plugins/<plugin-id>/` directory
57-
2. Add `meta.json` with required fields: `id`, `name`, `update_json`, optionally `description`, `homepage`, `tags`
58-
3. Run bot to generate `meta.generated.json` and `latest.json`
93+
2. Add `meta.json` with required fields: `id`, `name`, `updateUrl`, plus optionally `description`, `homepage`, `tags`, `patchedVersions`
94+
3. Run `pnpm run build` to generate outputs
5995
4. Commit and open PR
6096

61-
### Code Quality
62-
- **Linting**: `eslint` with `@antfu/eslint-config`, allows `console` statements
63-
- **Pre-commit**: Husky runs `lint-staged` on all changed files
64-
- Fix linting: `pnpm run lint:fix`
97+
### Testing & Validation
98+
- Schema validation is automatic in `loadPluginMeta()`
99+
- Version compatibility is computed in `extractCompatibility()`
100+
- XPI verification can be added later (currently a TODO in processor)
65101

66102
## Project Conventions
67103

68104
### TypeScript Patterns
69105
- **Monorepo imports**: Use workspace protocol, e.g., `@zotero-plugin-registry/shared`
70-
- **Module system**: ESM (`"type": "module"` in all package.json files)
106+
- **Module system**: ESM (`"type": "module"` in all package.json files), use `.js` extensions in imports
71107
- **Async/await**: Standard for all I/O operations
108+
- **Error handling**: Throw early with descriptive messages, caught and reported in ProcessResult
72109

73110
### File Organization
74-
- Shared types live in `shared/src/types.ts`, exported via package exports in `shared/package.json`
75-
- Schema generation: Run `scripts/generate-schema.sh` to create `meta.schema.json` from types
76-
- Plugin metadata format is validated against `meta.schema.json`
77-
78-
### Error Handling
79-
- **ProcessResult**: Captures both `success: string[]` and `errors: PluginError[]`
80-
- Continues processing remaining plugins even if one fails
81-
- Exit code 1 when errors occur (for CI/CD)
82-
83-
## Integration Points
84-
85-
### External Dependencies
86-
- **octokit**: GitHub API client (not actively used in current code but included)
87-
- **axios**: HTTP requests with auth headers
88-
- **adm-zip**: XPI file handling (structure for future use)
89-
- **globby**: File globbing for plugin discovery
90-
- **jsonc**: JSON with comments parsing
91-
- **es-toolkit**: Utility library
92-
- **fs-extra**: File system operations
93-
94-
### CI/CD Considerations
95-
- GITHUB_TOKEN must be available in environment for GitHub URL requests
96-
- Error reporting to GitHub issues/PRs (via `report.ts`, implementation details present)
97-
- Build artifacts: `meta.generated.json` and `latest.json` files per plugin
111+
- Shared types live in [shared/src/types.ts](shared/src/types.ts), exported via [shared/package.json](shared/package.json) exports
112+
- Schema generation: Run `pnpm run -C shared generate-schema` to create `meta.schema.json`
113+
- Bot modules are organized by concern: loaders, utils, core logic
114+
115+
### Import Ordering (ESLint enforced)
116+
1. Node builtins (`node:*`)
117+
2. External packages (alphabetically)
118+
3. Type imports
119+
4. Local imports (relative paths)
120+
121+
## Version Merging Strategy
122+
123+
When combining remote and patched versions:
124+
1. Build a map indexed by version number
125+
2. Add all remote versions first
126+
3. Apply patched versions (override if exists, add new if missing)
127+
4. Sort by semantic version (descending, using `semver` package)
128+
5. Return final merged list
129+
130+
This allows teams to patch remote update.json errors without waiting for upstream fixes.
131+
132+
## External Dependencies
133+
134+
- **commander**: CLI argument parsing
135+
- **consola**: Colored console output
136+
- **axios**: HTTP requests
137+
- **adm-zip**: XPI (ZIP) file parsing
138+
- **semver**: Semantic version comparison
139+
- **simple-git**: Git operations for PR detection
140+
- **octokit**: GitHub API client
141+
- **fs-extra**: Enhanced file system
142+
- **globby**: File pattern matching
143+
144+
## CI/CD Considerations
145+
146+
- **GITHUB_TOKEN** must be available for authenticated requests
147+
- **CI** env var distinguishes CI vs local environments
148+
- **GITHUB_EVENT_NAME** = 'pull_request' for PR validation
149+
- **GITHUB_REPOSITORY** = 'owner/repo' for API interactions
150+
- Build fails (exit code 1) if any plugin has errors
151+
- GitHub integration: PR comments on validation failure, issues for scheduled runs
98152

99153
## Common Task Patterns
100154

101-
**Adding features to processor**: Modify [bot/src/processor.ts](bot/src/processor.ts), ensure `ProcessResult` is properly populated with successes/errors
155+
**Modifying processor logic**: Edit [bot/src/processor.ts](bot/src/processor.ts), ensure ProcessResult properly tracks successes/errors
156+
157+
**Extending metadata support**: Update [shared/src/types.ts](shared/src/types.ts) interface, then regenerate schema
102158

103-
**Updating shared types**: Edit [shared/src/types.ts](shared/src/types.ts), then run schema generation to update validation
159+
**Adding new loader**: Create module in `loaders/`, export from [bot/src/loaders/index.ts](bot/src/loaders/index.ts)
104160

105-
**Debugging plugin processing**: Check `.cache.json` in plugin directory to understand last-fetch state; verify `update_json` URL format matches Zotero manifest structure
161+
**GitHub integration**: Use [bot/src/github-reporter.ts](bot/src/github-reporter.ts) for API calls via Octokit
106162

107-
**Working with pnpm**: Always use workspace commands: `pnpm run -C <package>` or `pnpm run --only <package>` for scoped tasks
163+
**Debugging plugin processing**: Check `.cache.json` in plugin directory and verify URL accessibility

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
"json.schemas": [
33
{
44
"fileMatch": ["plugins/**/meta.json"],
5-
"url": "./schemas/meta.schema.json"
5+
"url": "./shared/schemas/meta.schema.json"
66
}
77
],
88

9+
"prettier.enable": false,
910
"editor.formatOnSave": true,
1011
"editor.formatOnType": false,
1112
"editor.defaultFormatter": "dbaeumer.vscode-eslint",

bot/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,20 @@
2020
"test": "echo \"Error: no test specified\" && exit 1"
2121
},
2222
"dependencies": {
23+
"@types/semver": "^7.7.1",
2324
"adm-zip": "^0.5.16",
2425
"axios": "^1.13.2",
26+
"commander": "^14.0.2",
2527
"consola": "^3.4.2",
2628
"cross-env": "10.1.0",
2729
"es-toolkit": "^1.42.0",
2830
"fs-extra": "^11.3.2",
2931
"globby": "^15.0.0",
3032
"jsonc": "^2.0.0",
3133
"octokit": "^5.0.5",
32-
"ofetch": "^1.5.1"
34+
"ofetch": "^1.5.1",
35+
"semver": "^7.7.3",
36+
"simple-git": "^3.30.0"
3337
},
3438
"devDependencies": {
3539
"@types/adm-zip": "^0.5.7",

bot/src/build.ts

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,75 @@
1-
import path from "node:path";
2-
import process from "node:process";
3-
import fs from "fs-extra";
4-
import { PluginsRoot } from "./constant.ts";
5-
import { processPlugins } from "./processor.ts";
6-
import { report } from "./report.ts";
7-
8-
// TODO: more args
9-
// - `--dev` or no args: only fetch data, do not report to github
10-
// - `--all`: build all, report to github
11-
// - `--id xxx`: only build id
12-
// - `--pr`: for pr, auto detect changed files
13-
export async function main() {
14-
let ids: string[] = [];
15-
16-
const id = process.argv[2];
17-
if (id) {
18-
ids = [id];
19-
} else {
20-
const pluginsRoot = path.resolve(PluginsRoot);
21-
ids = await fs.readdir(pluginsRoot);
22-
}
1+
import path from 'node:path'
2+
import process from 'node:process'
3+
import consola from 'consola'
4+
import fs from 'fs-extra'
5+
import { PluginsRoot } from './constant.ts'
6+
import { processPlugins } from './processor.ts'
7+
import { report } from './reporters/index.ts'
238

24-
await buildAll(ids);
25-
}
9+
/**
10+
* Build all plugins or specific ones
11+
* @param ids Plugin IDs to process, or undefined to process all
12+
*/
13+
export async function buildPlugins(ids?: string[]): Promise<void> {
14+
try {
15+
let pluginIds = ids
2616

27-
async function buildAll(ids: string[]) {
28-
const result = await processPlugins(ids);
17+
// If no IDs specified, discover all plugins
18+
if (!pluginIds || pluginIds.length === 0) {
19+
pluginIds = await fs.readdir(PluginsRoot)
20+
pluginIds = pluginIds.filter((id) => {
21+
const pluginPath = path.join(PluginsRoot, id)
22+
return fs.statSync(pluginPath).isDirectory()
23+
})
24+
}
2925

30-
report(result);
26+
// Process plugins
27+
const result = await processPlugins(pluginIds)
3128

32-
// exit
33-
if (result.errors.length !== 0) process.exit(1);
29+
// Report results
30+
await report(result)
31+
32+
// Exit with appropriate code
33+
if (result.errors.length > 0) {
34+
process.exit(1)
35+
}
36+
}
37+
catch (error) {
38+
consola.error('Build failed:', error)
39+
process.exit(1)
40+
}
3441
}
3542

36-
// main();
43+
/**
44+
* Check mode for PR validation
45+
* Only process changed plugins and validate them
46+
*/
47+
export async function checkPlugins(ids?: string[]): Promise<void> {
48+
try {
49+
let pluginIds = ids
50+
51+
if (!pluginIds || pluginIds.length === 0) {
52+
// TODO: Use detectChangedPlugins() when git setup is available
53+
pluginIds = []
54+
}
55+
56+
if (pluginIds.length === 0) {
57+
return
58+
}
59+
60+
// Process plugins
61+
const result = await processPlugins(pluginIds)
62+
63+
// Report results
64+
await report(result)
65+
66+
// Exit with appropriate code
67+
if (result.errors.length > 0) {
68+
process.exit(1)
69+
}
70+
}
71+
catch (error) {
72+
consola.error('Check failed:', error)
73+
process.exit(1)
74+
}
75+
}

bot/src/cache.ts

Lines changed: 0 additions & 1 deletion
This file was deleted.

bot/src/cli.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11
#!/usr/bin/env node
22

3-
import { main } from "./build.ts";
3+
import process from 'node:process'
4+
import { Command } from 'commander'
5+
import { buildPlugins, checkPlugins } from './build.ts'
46

5-
main();
7+
const program = new Command()
8+
9+
program.name('zbot').description('Zotero Plugin Registry Bot').version('0.0.0')
10+
11+
program
12+
.command('build [plugins...]')
13+
.alias('b')
14+
.description('Build plugin metadata (default command)')
15+
.option('-a, --all', 'Build all plugins')
16+
.action(async (plugins, options) => {
17+
const ids = options.all ? undefined : Array.isArray(plugins) ? plugins : []
18+
await buildPlugins(ids)
19+
})
20+
21+
program
22+
.command('check [plugins...]')
23+
.alias('c')
24+
.description('Check mode: validate changed plugins for PRs')
25+
.action(async (plugins) => {
26+
const ids = Array.isArray(plugins) ? plugins : []
27+
await checkPlugins(ids)
28+
})
29+
30+
// Default command is build
31+
program.action(async () => {
32+
await buildPlugins()
33+
})
34+
35+
program.parse(process.argv)

bot/src/constant.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const PluginsRoot = "plugins";
1+
export const PluginsRoot = 'plugins'

bot/src/loaders/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { loadPluginMeta } from './meta.ts'
2+
export { loadUpdateJson } from './update-json.ts'

0 commit comments

Comments
 (0)