@nebulacomponents/citable 0.1.0 → 1.2.0
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/CHANGELOG.md +69 -0
- package/README.md +8 -2
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/manifest.json +5 -5
- package/dist/universal/.agents/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/manifest.json +5 -5
- package/dist/universal/.claude/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/manifest.json +5 -5
- package/dist/universal/.cursor/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/manifest.json +5 -5
- package/dist/universal/.gemini/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/manifest.json +5 -5
- package/dist/universal/.github/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/manifest.json +5 -5
- package/dist/universal/.kiro/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/manifest.json +5 -5
- package/dist/universal/.opencode/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/manifest.json +5 -5
- package/dist/universal/.pi/agent/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/manifest.json +5 -5
- package/dist/universal/.qoder/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/manifest.json +5 -5
- package/dist/universal/.rovodev/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/manifest.json +5 -5
- package/dist/universal/.trae/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/manifest.json +5 -5
- package/dist/universal/.trae-cn/skills/citable/schemas/finding.schema.json +253 -50
- package/dist/universal/manifest.json +51 -51
- package/package.json +4 -1
- package/schemas/finding.schema.json +253 -50
- package/src/cli/index.js +7 -0
- package/src/commands/selfUpgrade.js +146 -0
- package/src/crawler/fetch.js +42 -9
- package/src/detectors/agent.js +495 -0
- package/src/detectors/cwv.js +171 -0
- package/src/detectors/framework.js +1 -1
- package/src/detectors/hreflang.js +164 -0
- package/src/detectors/index.js +4 -1
- package/src/installer/index.js +7 -2
|
@@ -3,54 +3,184 @@
|
|
|
3
3
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
4
|
"title": "Finding",
|
|
5
5
|
"type": "object",
|
|
6
|
-
"required": [
|
|
6
|
+
"required": [
|
|
7
|
+
"finding_id",
|
|
8
|
+
"detector_id",
|
|
9
|
+
"run_id",
|
|
10
|
+
"timestamp",
|
|
11
|
+
"discipline",
|
|
12
|
+
"subject",
|
|
13
|
+
"observation",
|
|
14
|
+
"classification",
|
|
15
|
+
"remediation",
|
|
16
|
+
"verification",
|
|
17
|
+
"status"
|
|
18
|
+
],
|
|
7
19
|
"properties": {
|
|
8
|
-
"finding_id": {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
|
|
20
|
+
"finding_id": {
|
|
21
|
+
"$ref": "citable://schemas/common.defs.json#/definitions/id"
|
|
22
|
+
},
|
|
23
|
+
"detector_id": {
|
|
24
|
+
"$ref": "citable://schemas/common.defs.json#/definitions/id"
|
|
25
|
+
},
|
|
26
|
+
"run_id": {
|
|
27
|
+
"type": "string"
|
|
28
|
+
},
|
|
29
|
+
"timestamp": {
|
|
30
|
+
"type": "string"
|
|
31
|
+
},
|
|
32
|
+
"discipline": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"minItems": 1,
|
|
35
|
+
"items": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"enum": [
|
|
38
|
+
"seo",
|
|
39
|
+
"aeo",
|
|
40
|
+
"geo",
|
|
41
|
+
"agent-readiness"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
13
45
|
"subject": {
|
|
14
46
|
"type": "object",
|
|
15
|
-
"required": [
|
|
47
|
+
"required": [
|
|
48
|
+
"type",
|
|
49
|
+
"identifier"
|
|
50
|
+
],
|
|
16
51
|
"properties": {
|
|
17
|
-
"type": {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
"type": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"enum": [
|
|
55
|
+
"url",
|
|
56
|
+
"page",
|
|
57
|
+
"source_file",
|
|
58
|
+
"registry_entry",
|
|
59
|
+
"site",
|
|
60
|
+
"schema_block",
|
|
61
|
+
"link",
|
|
62
|
+
"crawler",
|
|
63
|
+
"claim",
|
|
64
|
+
"evidence",
|
|
65
|
+
"entity",
|
|
66
|
+
"prompt",
|
|
67
|
+
"query",
|
|
68
|
+
"experiment",
|
|
69
|
+
"run",
|
|
70
|
+
"file"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"identifier": {
|
|
74
|
+
"type": "string"
|
|
75
|
+
},
|
|
76
|
+
"url": {
|
|
77
|
+
"type": "string"
|
|
78
|
+
},
|
|
79
|
+
"source_file": {
|
|
80
|
+
"type": "string"
|
|
81
|
+
},
|
|
82
|
+
"source_location": {
|
|
83
|
+
"type": "string"
|
|
84
|
+
},
|
|
85
|
+
"rendered_selector": {
|
|
86
|
+
"type": "string"
|
|
87
|
+
}
|
|
23
88
|
}
|
|
24
89
|
},
|
|
25
90
|
"observation": {
|
|
26
91
|
"type": "object",
|
|
27
|
-
"required": [
|
|
92
|
+
"required": [
|
|
93
|
+
"summary",
|
|
94
|
+
"evidence"
|
|
95
|
+
],
|
|
28
96
|
"properties": {
|
|
29
|
-
"summary": {
|
|
30
|
-
|
|
97
|
+
"summary": {
|
|
98
|
+
"type": "string",
|
|
99
|
+
"minLength": 1
|
|
100
|
+
},
|
|
101
|
+
"evidence": {
|
|
102
|
+
"type": "array",
|
|
103
|
+
"minItems": 1,
|
|
104
|
+
"items": {
|
|
105
|
+
"type": "string"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
31
108
|
"captured_value": {},
|
|
32
109
|
"expected_value": {}
|
|
33
110
|
}
|
|
34
111
|
},
|
|
35
112
|
"classification": {
|
|
36
113
|
"type": "object",
|
|
37
|
-
"required": [
|
|
114
|
+
"required": [
|
|
115
|
+
"finding_type",
|
|
116
|
+
"severity",
|
|
117
|
+
"confidence",
|
|
118
|
+
"deterministic",
|
|
119
|
+
"impact"
|
|
120
|
+
],
|
|
38
121
|
"properties": {
|
|
39
|
-
"finding_type": {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
122
|
+
"finding_type": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"enum": [
|
|
125
|
+
"deterministic_observation",
|
|
126
|
+
"evidence_backed_semantic_finding",
|
|
127
|
+
"probabilistic_inference",
|
|
128
|
+
"strategic_hypothesis",
|
|
129
|
+
"experiment_result",
|
|
130
|
+
"untestable_condition"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
"severity": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"enum": [
|
|
136
|
+
"critical",
|
|
137
|
+
"high",
|
|
138
|
+
"medium",
|
|
139
|
+
"low",
|
|
140
|
+
"informational",
|
|
141
|
+
"experimental"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
"confidence": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"enum": [
|
|
147
|
+
"confirmed",
|
|
148
|
+
"high",
|
|
149
|
+
"medium",
|
|
150
|
+
"low",
|
|
151
|
+
"unknown"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"deterministic": {
|
|
155
|
+
"type": "boolean"
|
|
156
|
+
},
|
|
43
157
|
"impact": {
|
|
44
158
|
"type": "object",
|
|
45
159
|
"properties": {
|
|
46
|
-
"retrieval": {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
|
|
160
|
+
"retrieval": {
|
|
161
|
+
"$ref": "#/definitions/impactLevel"
|
|
162
|
+
},
|
|
163
|
+
"ranking": {
|
|
164
|
+
"$ref": "#/definitions/impactLevel"
|
|
165
|
+
},
|
|
166
|
+
"citation": {
|
|
167
|
+
"$ref": "#/definitions/impactLevel"
|
|
168
|
+
},
|
|
169
|
+
"representation": {
|
|
170
|
+
"$ref": "#/definitions/impactLevel"
|
|
171
|
+
},
|
|
172
|
+
"legal": {
|
|
173
|
+
"$ref": "#/definitions/impactLevel"
|
|
174
|
+
},
|
|
175
|
+
"reputational": {
|
|
176
|
+
"$ref": "#/definitions/impactLevel"
|
|
177
|
+
},
|
|
178
|
+
"conversion": {
|
|
179
|
+
"$ref": "#/definitions/impactLevel"
|
|
180
|
+
},
|
|
181
|
+
"maintainability": {
|
|
182
|
+
"$ref": "#/definitions/impactLevel"
|
|
183
|
+
}
|
|
54
184
|
}
|
|
55
185
|
}
|
|
56
186
|
}
|
|
@@ -58,44 +188,117 @@
|
|
|
58
188
|
"reasoning": {
|
|
59
189
|
"type": "object",
|
|
60
190
|
"properties": {
|
|
61
|
-
"applicable_requirement": {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
191
|
+
"applicable_requirement": {
|
|
192
|
+
"type": "string"
|
|
193
|
+
},
|
|
194
|
+
"explanation": {
|
|
195
|
+
"type": "string"
|
|
196
|
+
},
|
|
197
|
+
"assumptions": {
|
|
198
|
+
"type": "array",
|
|
199
|
+
"items": {
|
|
200
|
+
"type": "string"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"limitations": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"items": {
|
|
206
|
+
"type": "string"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
65
209
|
}
|
|
66
210
|
},
|
|
67
211
|
"remediation": {
|
|
68
212
|
"type": "object",
|
|
69
|
-
"required": [
|
|
213
|
+
"required": [
|
|
214
|
+
"preferred"
|
|
215
|
+
],
|
|
70
216
|
"properties": {
|
|
71
|
-
"preferred": {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
217
|
+
"preferred": {
|
|
218
|
+
"type": "string"
|
|
219
|
+
},
|
|
220
|
+
"alternatives": {
|
|
221
|
+
"type": "array",
|
|
222
|
+
"items": {
|
|
223
|
+
"type": "string"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
"unsafe_shortcuts": {
|
|
227
|
+
"type": "array",
|
|
228
|
+
"items": {
|
|
229
|
+
"type": "string"
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
"owner": {
|
|
233
|
+
"type": [
|
|
234
|
+
"string",
|
|
235
|
+
"null"
|
|
236
|
+
]
|
|
237
|
+
},
|
|
238
|
+
"review_required": {
|
|
239
|
+
"type": "boolean"
|
|
240
|
+
}
|
|
76
241
|
}
|
|
77
242
|
},
|
|
78
243
|
"verification": {
|
|
79
244
|
"type": "object",
|
|
80
|
-
"required": [
|
|
245
|
+
"required": [
|
|
246
|
+
"method"
|
|
247
|
+
],
|
|
81
248
|
"properties": {
|
|
82
|
-
"method": {
|
|
83
|
-
|
|
84
|
-
|
|
249
|
+
"method": {
|
|
250
|
+
"type": "string"
|
|
251
|
+
},
|
|
252
|
+
"expected_result": {
|
|
253
|
+
"type": "string"
|
|
254
|
+
},
|
|
255
|
+
"detector_to_rerun": {
|
|
256
|
+
"type": "string"
|
|
257
|
+
}
|
|
85
258
|
}
|
|
86
259
|
},
|
|
87
260
|
"status": {
|
|
88
261
|
"type": "object",
|
|
89
|
-
"required": [
|
|
262
|
+
"required": [
|
|
263
|
+
"state"
|
|
264
|
+
],
|
|
90
265
|
"properties": {
|
|
91
|
-
"state": {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
266
|
+
"state": {
|
|
267
|
+
"type": "string",
|
|
268
|
+
"enum": [
|
|
269
|
+
"open",
|
|
270
|
+
"acknowledged",
|
|
271
|
+
"in_remediation",
|
|
272
|
+
"resolved",
|
|
273
|
+
"accepted_risk",
|
|
274
|
+
"false_positive"
|
|
275
|
+
]
|
|
276
|
+
},
|
|
277
|
+
"first_seen": {
|
|
278
|
+
"type": "string"
|
|
279
|
+
},
|
|
280
|
+
"last_seen": {
|
|
281
|
+
"type": "string"
|
|
282
|
+
},
|
|
283
|
+
"resolved_at": {
|
|
284
|
+
"type": [
|
|
285
|
+
"string",
|
|
286
|
+
"null"
|
|
287
|
+
]
|
|
288
|
+
}
|
|
95
289
|
}
|
|
96
290
|
}
|
|
97
291
|
},
|
|
98
292
|
"definitions": {
|
|
99
|
-
"impactLevel": {
|
|
293
|
+
"impactLevel": {
|
|
294
|
+
"type": "string",
|
|
295
|
+
"enum": [
|
|
296
|
+
"none",
|
|
297
|
+
"low",
|
|
298
|
+
"medium",
|
|
299
|
+
"high",
|
|
300
|
+
"unknown"
|
|
301
|
+
]
|
|
302
|
+
}
|
|
100
303
|
}
|
|
101
|
-
}
|
|
304
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { inspect } from '../commands/inspect.js';
|
|
|
10
10
|
import { schemaCommand } from '../commands/schemaCmd.js';
|
|
11
11
|
import { compareSnapshots } from '../commands/compareSnapshots.js';
|
|
12
12
|
import { isInstallerCommand, runInstallerCommand } from '../installer/index.js';
|
|
13
|
+
import { selfUpgradeCommand } from '../commands/selfUpgrade.js';
|
|
13
14
|
|
|
14
15
|
const HELP = `citable — SEO / AEO / GEO audit, remediation, validation, and governance
|
|
15
16
|
|
|
@@ -31,6 +32,7 @@ Commands
|
|
|
31
32
|
schema Validate deployed JSON-LD and propose registry-derived schema
|
|
32
33
|
validate [mode] registries (default) | claims | evidence | schema | links
|
|
33
34
|
compare-snapshots [a b] Regression diff between two audit runs
|
|
35
|
+
self-upgrade Check for a newer version and upgrade the npx cache
|
|
34
36
|
|
|
35
37
|
Options
|
|
36
38
|
--target <dir|url> Built output directory or deployed URL to audit
|
|
@@ -115,6 +117,11 @@ export async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
115
117
|
if (r.summary.regression_critical_or_high > 0) process.exitCode = 1;
|
|
116
118
|
break;
|
|
117
119
|
}
|
|
120
|
+
case 'self-upgrade': {
|
|
121
|
+
const output = await selfUpgradeCommand(argv.slice(1));
|
|
122
|
+
console.log(output);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
118
125
|
case undefined:
|
|
119
126
|
console.log(HELP);
|
|
120
127
|
break;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* self-upgrade command
|
|
3
|
+
*
|
|
4
|
+
* Checks npm for the latest @nebulacomponents/citable version and upgrades
|
|
5
|
+
* the running npx cache if a newer version is available.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx @nebulacomponents/citable self-upgrade
|
|
9
|
+
* npx @nebulacomponents/citable self-upgrade --check (check only, no upgrade)
|
|
10
|
+
* npx @nebulacomponents/citable self-upgrade --json (machine-readable output)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const PACKAGE_NAME = '@nebulacomponents/citable';
|
|
19
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
20
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fetch the latest published version from the npm registry.
|
|
24
|
+
* Uses Node's built-in fetch (Node 22+); no extra dependencies.
|
|
25
|
+
*
|
|
26
|
+
* @returns {Promise<string>} latest semver string
|
|
27
|
+
*/
|
|
28
|
+
async function fetchLatestVersion() {
|
|
29
|
+
const url = `${REGISTRY}/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(url, {
|
|
34
|
+
headers: { Accept: 'application/json' },
|
|
35
|
+
signal: controller.signal,
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) throw new Error(`registry returned ${res.status}`);
|
|
38
|
+
const data = await res.json();
|
|
39
|
+
if (!data.version) throw new Error('no version field in registry response');
|
|
40
|
+
return data.version;
|
|
41
|
+
} finally {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Compare two semver strings. Returns:
|
|
48
|
+
* 1 if a > b
|
|
49
|
+
* 0 if a === b
|
|
50
|
+
* -1 if a < b
|
|
51
|
+
*/
|
|
52
|
+
function compareSemver(a, b) {
|
|
53
|
+
const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
|
|
54
|
+
const [aMaj, aMin, aPatch] = parse(a);
|
|
55
|
+
const [bMaj, bMin, bPatch] = parse(b);
|
|
56
|
+
if (aMaj !== bMaj) return aMaj > bMaj ? 1 : -1;
|
|
57
|
+
if (aMin !== bMin) return aMin > bMin ? 1 : -1;
|
|
58
|
+
if (aPatch !== bPatch) return aPatch > bPatch ? 1 : -1;
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Re-execute the current command via npx at the new version.
|
|
64
|
+
* Used to trigger the npx cache refresh as a side-effect.
|
|
65
|
+
*/
|
|
66
|
+
function triggerNpxRefresh(latestVersion) {
|
|
67
|
+
// npx --yes ensures no prompt; @version pins to latest
|
|
68
|
+
const result = spawnSync(
|
|
69
|
+
'npx',
|
|
70
|
+
['--yes', `${PACKAGE_NAME}@${latestVersion}`, '--version'],
|
|
71
|
+
{ stdio: 'pipe', timeout: 60_000 },
|
|
72
|
+
);
|
|
73
|
+
return result.status === 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function selfUpgradeCommand(args) {
|
|
77
|
+
const checkOnly = args.includes('--check');
|
|
78
|
+
const jsonOutput = args.includes('--json');
|
|
79
|
+
|
|
80
|
+
// Resolve current version from our own package.json
|
|
81
|
+
let currentVersion;
|
|
82
|
+
try {
|
|
83
|
+
const req = createRequire(import.meta.url);
|
|
84
|
+
const pkgPath = path.resolve(fileURLToPath(import.meta.url), '../../../package.json');
|
|
85
|
+
currentVersion = req(pkgPath).version;
|
|
86
|
+
} catch {
|
|
87
|
+
const err = { ok: false, error: 'could not read local package.json version' };
|
|
88
|
+
if (jsonOutput) return JSON.stringify(err, null, 2);
|
|
89
|
+
return `error: ${err.error}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Fetch latest from registry
|
|
93
|
+
let latestVersion;
|
|
94
|
+
try {
|
|
95
|
+
latestVersion = await fetchLatestVersion();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const result = { ok: false, current: currentVersion, error: `registry fetch failed: ${err.message}` };
|
|
98
|
+
if (jsonOutput) return JSON.stringify(result, null, 2);
|
|
99
|
+
return `error: ${result.error}\nCurrent version: ${currentVersion}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const cmp = compareSemver(latestVersion, currentVersion);
|
|
103
|
+
|
|
104
|
+
if (cmp <= 0) {
|
|
105
|
+
// Already current or ahead (local dev)
|
|
106
|
+
const result = { ok: true, current: currentVersion, latest: latestVersion, status: 'current' };
|
|
107
|
+
if (jsonOutput) return JSON.stringify(result, null, 2);
|
|
108
|
+
return `citable ${currentVersion} is already up to date (latest: ${latestVersion})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Update available
|
|
112
|
+
if (checkOnly) {
|
|
113
|
+
const result = { ok: true, current: currentVersion, latest: latestVersion, status: 'update-available' };
|
|
114
|
+
if (jsonOutput) return JSON.stringify(result, null, 2);
|
|
115
|
+
return [
|
|
116
|
+
`Update available: ${currentVersion} → ${latestVersion}`,
|
|
117
|
+
`Run: npx ${PACKAGE_NAME}@latest self-upgrade`,
|
|
118
|
+
].join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Perform upgrade: warm the npx cache at the new version
|
|
122
|
+
const refreshed = triggerNpxRefresh(latestVersion);
|
|
123
|
+
|
|
124
|
+
const result = {
|
|
125
|
+
ok: refreshed,
|
|
126
|
+
current: currentVersion,
|
|
127
|
+
latest: latestVersion,
|
|
128
|
+
status: refreshed ? 'upgraded' : 'upgrade-failed',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (jsonOutput) return JSON.stringify(result, null, 2);
|
|
132
|
+
|
|
133
|
+
if (refreshed) {
|
|
134
|
+
return [
|
|
135
|
+
`citable upgraded: ${currentVersion} → ${latestVersion}`,
|
|
136
|
+
`Run your next command with: npx ${PACKAGE_NAME}@latest <command>`,
|
|
137
|
+
`Or pin globally: npm install -g ${PACKAGE_NAME}@latest`,
|
|
138
|
+
].join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [
|
|
142
|
+
`Upgrade attempt failed (npx returned non-zero).`,
|
|
143
|
+
`Current: ${currentVersion} Latest: ${latestVersion}`,
|
|
144
|
+
`Manual upgrade: npx ${PACKAGE_NAME}@${latestVersion} <command>`,
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
package/src/crawler/fetch.js
CHANGED
|
@@ -2,28 +2,61 @@
|
|
|
2
2
|
* Fetch a URL capturing status, headers, body, and redirect chain.
|
|
3
3
|
* Used only when the operator supplies a deployed target and fetching is permitted.
|
|
4
4
|
*/
|
|
5
|
-
export async function fetchUrl(url, { userAgent = 'CitableAudit/0.1', maxRedirects = 10, timeoutMs = 20000 } = {}) {
|
|
5
|
+
export async function fetchUrl(url, { userAgent = 'CitableAudit/0.1', maxRedirects = 10, timeoutMs = 20000, maxRetries = 3, retryDelayMs = 1000 } = {}) {
|
|
6
6
|
const chain = [];
|
|
7
7
|
let current = url;
|
|
8
|
+
|
|
8
9
|
for (let i = 0; i <= maxRedirects; i++) {
|
|
9
10
|
const controller = new AbortController();
|
|
10
11
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
11
12
|
let res;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
let lastError;
|
|
14
|
+
|
|
15
|
+
// Retry loop for transient failures
|
|
16
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
17
|
+
try {
|
|
18
|
+
res = await fetch(current, {
|
|
19
|
+
redirect: 'manual',
|
|
20
|
+
headers: { 'user-agent': userAgent },
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
lastError = null;
|
|
24
|
+
break; // Success, break retry loop
|
|
25
|
+
} catch (err) {
|
|
26
|
+
lastError = err;
|
|
27
|
+
// Only retry on network errors or 5xx, not on 4xx
|
|
28
|
+
if (err.name === 'AbortError') {
|
|
29
|
+
throw err; // Timeout, don't retry
|
|
30
|
+
}
|
|
31
|
+
// Exponential backoff with jitter
|
|
32
|
+
const delay = retryDelayMs * Math.pow(2, attempt) + Math.random() * retryDelayMs;
|
|
33
|
+
if (attempt < maxRetries - 1) {
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
35
|
+
}
|
|
36
|
+
} finally {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
}
|
|
20
39
|
}
|
|
40
|
+
|
|
41
|
+
if (lastError) {
|
|
42
|
+
throw lastError;
|
|
43
|
+
}
|
|
44
|
+
|
|
21
45
|
const headers = Object.fromEntries(res.headers.entries());
|
|
22
46
|
if (res.status >= 300 && res.status < 400 && headers.location) {
|
|
23
47
|
chain.push({ url: current, status: res.status, location: headers.location });
|
|
24
48
|
current = new URL(headers.location, current).href;
|
|
25
49
|
continue;
|
|
26
50
|
}
|
|
51
|
+
|
|
52
|
+
// Retry on 5xx with one less attempt (already used retry loop above)
|
|
53
|
+
if (res.status >= 500 && i < maxRedirects) {
|
|
54
|
+
// Treat 5xx as transient, add to chain and retry same URL
|
|
55
|
+
chain.push({ url: current, status: res.status, location: null, error: 'server error' });
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
const body = await res.text();
|
|
28
61
|
return { url: current, requestedUrl: url, status: res.status, headers, body, redirectChain: chain };
|
|
29
62
|
}
|