@extenshi/cli 0.21.0 → 0.22.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/README.md +4 -0
- package/dist/amo-source-detect.d.ts +18 -0
- package/dist/amo-source-detect.d.ts.map +1 -0
- package/dist/amo-source-detect.js +41 -0
- package/dist/amo-source-detect.js.map +1 -0
- package/dist/amo-source.d.ts +41 -0
- package/dist/amo-source.d.ts.map +1 -0
- package/dist/amo-source.js +240 -0
- package/dist/amo-source.js.map +1 -0
- package/dist/amowebstore-md.d.ts +38 -0
- package/dist/amowebstore-md.d.ts.map +1 -0
- package/dist/amowebstore-md.js +118 -0
- package/dist/amowebstore-md.js.map +1 -0
- package/dist/auto-evidence.d.ts +100 -0
- package/dist/auto-evidence.d.ts.map +1 -0
- package/dist/auto-evidence.js +197 -0
- package/dist/auto-evidence.js.map +1 -0
- package/dist/cli.js +265 -8
- package/dist/cli.js.map +1 -1
- package/dist/edgewebstore-md.d.ts +26 -0
- package/dist/edgewebstore-md.d.ts.map +1 -0
- package/dist/edgewebstore-md.js +95 -0
- package/dist/edgewebstore-md.js.map +1 -0
- package/dist/evidence-ingest.d.ts +60 -0
- package/dist/evidence-ingest.d.ts.map +1 -0
- package/dist/evidence-ingest.js +141 -0
- package/dist/evidence-ingest.js.map +1 -0
- package/dist/firefox-rdp.d.ts +55 -0
- package/dist/firefox-rdp.d.ts.map +1 -0
- package/dist/firefox-rdp.js +203 -0
- package/dist/firefox-rdp.js.map +1 -0
- package/dist/gecko-id.d.ts +55 -0
- package/dist/gecko-id.d.ts.map +1 -0
- package/dist/gecko-id.js +132 -0
- package/dist/gecko-id.js.map +1 -0
- package/dist/generate-listing.d.ts +19 -8
- package/dist/generate-listing.d.ts.map +1 -1
- package/dist/generate-listing.js +66 -18
- package/dist/generate-listing.js.map +1 -1
- package/dist/icon.d.ts +18 -0
- package/dist/icon.d.ts.map +1 -1
- package/dist/icon.js +29 -20
- package/dist/icon.js.map +1 -1
- package/dist/media.d.ts +42 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +96 -0
- package/dist/media.js.map +1 -0
- package/dist/package.d.ts +163 -0
- package/dist/package.d.ts.map +1 -0
- package/dist/package.js +500 -0
- package/dist/package.js.map +1 -0
- package/dist/page.d.ts +10 -0
- package/dist/page.d.ts.map +1 -1
- package/dist/page.js +32 -2
- package/dist/page.js.map +1 -1
- package/dist/pay-contract.d.ts +10 -3
- package/dist/pay-contract.d.ts.map +1 -1
- package/dist/pay-contract.js +9 -0
- package/dist/pay-contract.js.map +1 -1
- package/dist/pay-test-cycle.d.ts +40 -0
- package/dist/pay-test-cycle.d.ts.map +1 -0
- package/dist/pay-test-cycle.js +207 -0
- package/dist/pay-test-cycle.js.map +1 -0
- package/dist/pay.d.ts +1 -1
- package/dist/pay.d.ts.map +1 -1
- package/dist/pay.js +1 -0
- package/dist/pay.js.map +1 -1
- package/dist/project.d.ts +39 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +231 -20
- package/dist/project.js.map +1 -1
- package/dist/publish.d.ts +33 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +113 -2
- package/dist/publish.js.map +1 -1
- package/dist/review-checks.d.ts +13 -0
- package/dist/review-checks.d.ts.map +1 -1
- package/dist/review-checks.js.map +1 -1
- package/dist/review-risk.d.ts +10 -1
- package/dist/review-risk.d.ts.map +1 -1
- package/dist/review-risk.js +1 -0
- package/dist/review-risk.js.map +1 -1
- package/dist/scan.d.ts +3 -2
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +4 -2
- package/dist/scan.js.map +1 -1
- package/dist/screenshots-scenes.d.ts +1383 -0
- package/dist/screenshots-scenes.d.ts.map +1 -0
- package/dist/screenshots-scenes.js +276 -0
- package/dist/screenshots-scenes.js.map +1 -0
- package/dist/screenshots.d.ts +145 -0
- package/dist/screenshots.d.ts.map +1 -0
- package/dist/screenshots.js +557 -0
- package/dist/screenshots.js.map +1 -0
- package/dist/smoke.d.ts +81 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +491 -0
- package/dist/smoke.js.map +1 -0
- package/dist/store-assets.d.ts +139 -0
- package/dist/store-assets.d.ts.map +1 -0
- package/dist/store-assets.js +582 -0
- package/dist/store-assets.js.map +1 -0
- package/dist/zip.d.ts +57 -0
- package/dist/zip.d.ts.map +1 -0
- package/dist/zip.js +134 -0
- package/dist/zip.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-evidence.d.ts","sourceRoot":"","sources":["../src/auto-evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAMH,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAE5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAIlD,OAAO,EAAE,KAAK,aAAa,EAAwB,MAAM,oBAAoB,CAAA;AAC7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAA;AAG3D,eAAO,MAAM,SAAS,GAAI,OAAO,UAAU,GAAG,MAAM,WAAqD,CAAA;AAEzG;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC/B,YAAY,EAAE,MAAM,EACpB,cAAc,EAAE,MAAM,EACtB,QAAQ,CAAC,EAAE,MAAM,GACf,eAAe,EAAE,CAmBnB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE;IACnC,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC7B,GAAG,cAAc,EAAE,CAWnB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,GAAG,cAAc,EAAE,CAUxG;AAQD,KAAK,eAAe,GAAG,WAAW,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAA;AAE7D,gGAAgG;AAChG,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,eAAe,CAMlF;AAKD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IAC1C,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAA;IAClC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAA;CACpC,GAAG,cAAc,EAAE,CAYnB;AAED,uGAAuG;AACvG,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,GAAG,IAAI,CAEnE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IACtC,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,UAAU,GAAG,MAAM,CAAA;IACjC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,QAAQ,EAAE,eAAe,EAAE,CAAA;IAC3B,2DAA2D;IAC3D,eAAe,EAAE,SAAS,aAAa,EAAE,CAAA;CACzC,GAAG,cAAc,EAAE,CAwBnB;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,KAAK,EAAE;IACrD,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,eAAe,EAAE,CAAA;CAC3B,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAU5B;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,GAAG,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBtG"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--evidence` on `scan`, `review-risk`, `generate-listing` and `test smoke`:
|
|
3
|
+
* turn the result the command just computed into the readiness evidence kind it
|
|
4
|
+
* attests, with the digests `releaseReadiness` matches against, and record it for
|
|
5
|
+
* the project bound in this directory — through the same client path as
|
|
6
|
+
* `extenshi evidence push` (project.ts → recordAgentEvidence).
|
|
7
|
+
*
|
|
8
|
+
* The digests are the contract (shared-types/project-workspace.ts
|
|
9
|
+
* `releaseReadiness`):
|
|
10
|
+
*
|
|
11
|
+
* kind artifactDigest inputDigest locale
|
|
12
|
+
* scan sha256(zip) sha256(zip) —
|
|
13
|
+
* installed sha256(zip) sha256(zip) —
|
|
14
|
+
* permissions sha256(zip) hashMetadata(packaged manifest) —
|
|
15
|
+
* listing sha256(zip) sha256(listing file bytes) each locale
|
|
16
|
+
*
|
|
17
|
+
* `hashMetadata(manifest)` is what `release prepare` stores as manifestDigest,
|
|
18
|
+
* and `sha256(listing file)` is what `release prepare --listing <file>` stores as
|
|
19
|
+
* listingDigest — so evidence recorded here is current exactly when the release
|
|
20
|
+
* binds the same package and the same listing file.
|
|
21
|
+
*
|
|
22
|
+
* Status rules (documented in the CLI docs):
|
|
23
|
+
* scan failed on a REJECTION_LIKELY store-compliance finding or any
|
|
24
|
+
* CRITICAL security finding — the report's own "fail" verdict.
|
|
25
|
+
* permissions failed on a REJECTED review-risk finding outside the listing rules.
|
|
26
|
+
* listing failed only on a REJECTED listing-rule finding for that locale —
|
|
27
|
+
* exactly what would make `review-risk` exit non-zero. Listing rules
|
|
28
|
+
* are ADVISORY by roadmap policy (iteration 2 T9: the v3 dictionary
|
|
29
|
+
* measured a 78.7% false-positive rate) and never fail review-risk,
|
|
30
|
+
* so failing readiness on them would block releases on false
|
|
31
|
+
* positives. An advisory finding leaves the check `passed`.
|
|
32
|
+
* installed the smoke result.
|
|
33
|
+
*
|
|
34
|
+
* `permissions` and `listing` carry a `summary` (finding count, blocking count,
|
|
35
|
+
* distinct check ids) so the advisories behind a `passed` stay visible to
|
|
36
|
+
* whoever reads the record. It is sent only to a server that advertises it
|
|
37
|
+
* (project.ts → recordAgentEvidence).
|
|
38
|
+
*/
|
|
39
|
+
import { createHash } from 'node:crypto';
|
|
40
|
+
import fs from 'node:fs';
|
|
41
|
+
import { hashMetadata } from '@extenshi/contracts';
|
|
42
|
+
import { InvalidInputError } from './expected-error.js';
|
|
43
|
+
import { assembleListing } from './listing.js';
|
|
44
|
+
import { packagedBrowsersFor } from './package.js';
|
|
45
|
+
import { summarizeCompliance, summarizeReport } from './render.js';
|
|
46
|
+
import { computeVerdict } from './report.js';
|
|
47
|
+
import { loadArtifact } from './review-archive.js';
|
|
48
|
+
import { runListingOnlyChecks } from './review-checks.js';
|
|
49
|
+
const BROWSERS = ['chrome', 'edge', 'firefox'];
|
|
50
|
+
export const sha256Hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
51
|
+
/**
|
|
52
|
+
* Which store(s) the evidence is for: the explicit list, else the browsers
|
|
53
|
+
* `extenshi package` recorded for these exact bytes (Chrome and Edge built from
|
|
54
|
+
* one directory are the same zip), else refuse — mislabelled evidence would
|
|
55
|
+
* satisfy the wrong release.
|
|
56
|
+
*/
|
|
57
|
+
export function evidenceBrowsers(artifactPath, artifactDigest, explicit) {
|
|
58
|
+
if (explicit) {
|
|
59
|
+
const list = explicit
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((s) => s.trim().toLowerCase())
|
|
62
|
+
.filter(Boolean);
|
|
63
|
+
const bad = list.filter((s) => !BROWSERS.includes(s));
|
|
64
|
+
if (bad.length || !list.length)
|
|
65
|
+
throw new InvalidInputError(`--evidence-browser takes a comma-separated list of: ${BROWSERS.join(', ')}`);
|
|
66
|
+
return [...new Set(list)];
|
|
67
|
+
}
|
|
68
|
+
const packaged = packagedBrowsersFor(artifactPath, artifactDigest);
|
|
69
|
+
if (packaged.length)
|
|
70
|
+
return packaged;
|
|
71
|
+
throw new InvalidInputError('--evidence needs to know which store this package is for. Pass --evidence-browser chrome|edge|firefox, ' +
|
|
72
|
+
'or run it on a zip from `extenshi package`, whose extenshi-packages.json records the browser.');
|
|
73
|
+
}
|
|
74
|
+
export function scanEvidence(input) {
|
|
75
|
+
const verdict = computeVerdict(summarizeCompliance(input.body), summarizeReport(input.body));
|
|
76
|
+
const critical = summarizeReport(input.body).findings.some((f) => f.severity === 'CRITICAL');
|
|
77
|
+
const status = verdict.level === 'fail' || critical ? 'failed' : 'passed';
|
|
78
|
+
return input.browsers.map((browser) => ({
|
|
79
|
+
kind: 'scan',
|
|
80
|
+
browser,
|
|
81
|
+
artifactDigest: input.artifactDigest,
|
|
82
|
+
inputDigest: input.artifactDigest,
|
|
83
|
+
status,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
export function smokeEvidence(input) {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
kind: 'installed',
|
|
90
|
+
browser: input.browser,
|
|
91
|
+
artifactDigest: input.result.sha256,
|
|
92
|
+
inputDigest: input.result.sha256,
|
|
93
|
+
status: input.result.status,
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Mirrors EVIDENCE_SUMMARY_MAX_IDS in the contract. Not imported: an installed
|
|
99
|
+
* @extenshi/contracts that predates the summary has no such export, and a
|
|
100
|
+
* missing named ESM import would stop the whole CLI from loading.
|
|
101
|
+
*/
|
|
102
|
+
const SUMMARY_MAX_IDS = 50;
|
|
103
|
+
/** Counts and distinct check ids — never finding text, which can quote listing copy or code. */
|
|
104
|
+
export function findingSummary(findings) {
|
|
105
|
+
return {
|
|
106
|
+
findingCount: findings.length,
|
|
107
|
+
blockingCount: findings.filter(isBlocking).length,
|
|
108
|
+
findingIds: [...new Set(findings.map((f) => f.check))].sort().slice(0, SUMMARY_MAX_IDS),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** What makes `review-risk` exit non-zero (summarize → `blocking`). */
|
|
112
|
+
const isBlocking = (f) => f.verdict === 'REJECTED';
|
|
113
|
+
export function permissionsEvidence(input) {
|
|
114
|
+
const own = input.findings.filter((f) => !input.listingCheckIds.has(f.check));
|
|
115
|
+
const summary = findingSummary(own);
|
|
116
|
+
const inputDigest = hashMetadata(input.manifest);
|
|
117
|
+
return input.browsers.map((browser) => ({
|
|
118
|
+
kind: 'permissions',
|
|
119
|
+
browser,
|
|
120
|
+
artifactDigest: input.artifactDigest,
|
|
121
|
+
inputDigest,
|
|
122
|
+
status: summary.blockingCount ? 'failed' : 'passed',
|
|
123
|
+
summary,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
/** Listing findings carry `[locale] field…` evidence; one without a locale applies to every locale. */
|
|
127
|
+
export function findingLocale(finding) {
|
|
128
|
+
return /^\[([^\]\s]+)\]/.exec(finding.evidence ?? '')?.[1] ?? null;
|
|
129
|
+
}
|
|
130
|
+
export function listingEvidence(input) {
|
|
131
|
+
const locales = input.listing?.locales.map((l) => l.locale) ?? [];
|
|
132
|
+
if (!locales.length)
|
|
133
|
+
throw new InvalidInputError('The listing has no locale with text, so there is nothing to attest.');
|
|
134
|
+
const inputDigest = sha256Hex(input.listingBytes);
|
|
135
|
+
return input.browsers.flatMap((browser) => locales.map((locale) => {
|
|
136
|
+
const hits = input.listingFindings.filter((f) => {
|
|
137
|
+
const at = findingLocale(f);
|
|
138
|
+
return at === null || at === locale;
|
|
139
|
+
});
|
|
140
|
+
const summary = findingSummary(hits);
|
|
141
|
+
return {
|
|
142
|
+
kind: 'listing',
|
|
143
|
+
browser,
|
|
144
|
+
locale,
|
|
145
|
+
artifactDigest: input.artifactDigest,
|
|
146
|
+
inputDigest,
|
|
147
|
+
// Advisory findings are carried in the summary, never in the status.
|
|
148
|
+
status: summary.blockingCount ? 'failed' : 'passed',
|
|
149
|
+
summary,
|
|
150
|
+
};
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `generate-listing --evidence`: read the draft the command just wrote
|
|
155
|
+
* (CHROMEWEBSTORE.md, EDGE.md or AMO.md — one parser, keyed on headings) back
|
|
156
|
+
* over the package, run the listing rules on it and bind the evidence to that
|
|
157
|
+
* file's bytes. `browsers` is the store the draft is for.
|
|
158
|
+
*/
|
|
159
|
+
export async function generatedListingEvidence(input) {
|
|
160
|
+
const { manifest, files } = await loadArtifact(input.artifactBytes);
|
|
161
|
+
const listing = assembleListing(manifest, files, input.draftPath);
|
|
162
|
+
return listingEvidence({
|
|
163
|
+
artifactDigest: input.artifactDigest,
|
|
164
|
+
listingBytes: fs.readFileSync(input.draftPath),
|
|
165
|
+
listing,
|
|
166
|
+
browsers: input.browsers,
|
|
167
|
+
listingFindings: runListingOnlyChecks(listing),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Record the batch for the bound project and say so on stderr (stdout stays the
|
|
172
|
+
* command's own output, e.g. --json). A queued (offline) record sets exit code 1
|
|
173
|
+
* like `evidence push`; any other failure throws — the caller asked for evidence.
|
|
174
|
+
*/
|
|
175
|
+
export async function recordAutoEvidence(records, cwd = process.cwd()) {
|
|
176
|
+
if (!records.length)
|
|
177
|
+
return;
|
|
178
|
+
const { recordAgentEvidence } = await import('./project.js');
|
|
179
|
+
const result = await recordAgentEvidence(cwd, records);
|
|
180
|
+
const summary = records
|
|
181
|
+
.map((r) => {
|
|
182
|
+
const advisories = r.summary ? r.summary.findingCount - r.summary.blockingCount : 0;
|
|
183
|
+
const note = advisories ? ` (${advisories} advisory: ${r.summary?.findingIds.join(' ')})` : '';
|
|
184
|
+
return `${r.kind}${r.locale ? `:${r.locale}` : ''}/${r.browser}=${r.status}${note}`;
|
|
185
|
+
})
|
|
186
|
+
.join(', ');
|
|
187
|
+
if (!result.summaries && records.some((r) => r.summary))
|
|
188
|
+
process.stderr.write('note: this server does not store evidence summaries yet — the advisory findings above are shown here only\n');
|
|
189
|
+
if (result.status === 'pending-sync') {
|
|
190
|
+
process.stderr.write(`evidence queued (offline) for project ${result.projectId}: ${summary}. Run extenshi project sync.\n`);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
process.stderr.write(`evidence recorded for project ${result.projectId}: ${summary}\n`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=auto-evidence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-evidence.js","sourceRoot":"","sources":["../src/auto-evidence.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -27,14 +27,19 @@
|
|
|
27
27
|
* re-reads the config on the first event of the run, so an answer given here is
|
|
28
28
|
* already in force for the very run that asked it.
|
|
29
29
|
*/
|
|
30
|
+
import { existsSync, statSync } from 'node:fs';
|
|
30
31
|
import { readFile } from 'node:fs/promises';
|
|
31
32
|
import { createRequire } from 'node:module';
|
|
32
33
|
import os from 'node:os';
|
|
34
|
+
import path from 'node:path';
|
|
33
35
|
import * as readline from 'node:readline/promises';
|
|
34
36
|
import { program } from 'commander';
|
|
37
|
+
import { runAmoSource } from './amo-source.js';
|
|
38
|
+
import { evidenceBrowsers, generatedListingEvidence, listingEvidence, permissionsEvidence, recordAutoEvidence, scanEvidence, sha256Hex, smokeEvidence, } from './auto-evidence.js';
|
|
35
39
|
import { checkAutoUpdate } from './auto-update.js';
|
|
36
40
|
import { CONFIG_PATH, loadConfig, readTelemetryPreference, saveConfig, saveTelemetryPreference, } from './config.js';
|
|
37
|
-
import {
|
|
41
|
+
import { registerEvidenceIngest } from './evidence-ingest.js';
|
|
42
|
+
import { InvalidInputError, isExpectedError } from './expected-error.js';
|
|
38
43
|
import { runGenerateListing } from './generate-listing.js';
|
|
39
44
|
import { runIconPreview } from './icon.js';
|
|
40
45
|
import { applyLocalization, checkLocalization, prepareLocalization } from './localize.js';
|
|
@@ -42,13 +47,20 @@ import { initializeLocalization } from './localize-init.js';
|
|
|
42
47
|
import { makeLoginBff } from './login-bff.js';
|
|
43
48
|
import { runBrowserLogin } from './login-flow.js';
|
|
44
49
|
import { openBrowser, openLoopback, sleep } from './login-runtime.js';
|
|
50
|
+
import { runMediaUpload } from './media.js';
|
|
51
|
+
import { parsePackageBrowsers, resolvePackagedArtifact, runPackage } from './package.js';
|
|
45
52
|
import { runPageGenerate, runPagePublish, runPageRegister, runPageUnpublish } from './page.js';
|
|
46
53
|
import { registerPayCommands } from './pay.js';
|
|
54
|
+
import { registerPayTestCycle } from './pay-test-cycle.js';
|
|
47
55
|
import { registerProjectCommands } from './project.js';
|
|
48
56
|
import { runPublish } from './publish.js';
|
|
57
|
+
import { runListingOnlyChecks } from './review-checks.js';
|
|
49
58
|
import { runReviewRisk } from './review-risk.js';
|
|
50
59
|
import { parseRefs, parseStore, runRisk } from './risk.js';
|
|
51
60
|
import { runScan } from './scan.js';
|
|
61
|
+
import { runScreenshotsCommand } from './screenshots.js';
|
|
62
|
+
import { ENGINE_BROWSER, parseSmokeEngine, renderSmoke, runSmoke } from './smoke.js';
|
|
63
|
+
import { runStoreAssets } from './store-assets.js';
|
|
52
64
|
import { captureError, captureEvent, classifyError, flagsFromArgv, flushTelemetry, initTelemetry, } from './telemetry.js';
|
|
53
65
|
import { defaultConsentDeps, envAnswerSource, maybeAskForTelemetryConsent } from './telemetry-consent.js';
|
|
54
66
|
const MISSING_KEY_HELP = "Looks like you don't have an Extenshi API key set up yet — let's fix that.\n" +
|
|
@@ -102,8 +114,24 @@ async function runCommand(command, action) {
|
|
|
102
114
|
process.exit(1);
|
|
103
115
|
}
|
|
104
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* `--evidence` is checked BEFORE the work it attests: a paid scan that cannot
|
|
119
|
+
* record its evidence afterwards would spend a credit for nothing.
|
|
120
|
+
*/
|
|
121
|
+
function assertEvidenceBinding() {
|
|
122
|
+
if (!existsSync(path.join(process.cwd(), '.extenshi/project.json')))
|
|
123
|
+
throw new InvalidInputError('--evidence records against the project bound in this directory. Run extenshi project bind --project <id> --repo <owner/repo> first.');
|
|
124
|
+
}
|
|
125
|
+
/** Evidence binds to the exact package bytes, so it needs a package file, not an unpacked folder. */
|
|
126
|
+
async function evidenceArtifact(artifact) {
|
|
127
|
+
if (!existsSync(artifact) || !statSync(artifact).isFile())
|
|
128
|
+
throw new InvalidInputError('--evidence binds to the exact package: pass the zip (e.g. from extenshi package), not an unpacked folder.');
|
|
129
|
+
const bytes = await readFile(artifact);
|
|
130
|
+
return { bytes, digest: sha256Hex(bytes) };
|
|
131
|
+
}
|
|
105
132
|
registerProjectCommands(program);
|
|
106
|
-
registerPayCommands(program);
|
|
133
|
+
registerPayTestCycle(registerPayCommands(program));
|
|
134
|
+
registerEvidenceIngest(program, runCommand);
|
|
107
135
|
program
|
|
108
136
|
.name('extenshi')
|
|
109
137
|
.description('Pre-publish security scanner for browser extensions')
|
|
@@ -125,12 +153,21 @@ program
|
|
|
125
153
|
.option('--listing <file>', 'JSON file with your store listing text (name / shortDescription / description, optionally per locale). ' +
|
|
126
154
|
'Without it the listing checks still run, but only over the name and short description the package carries — ' +
|
|
127
155
|
'the full description lives in the dashboard, never in the artifact')
|
|
156
|
+
.option('--evidence', 'Record the result as `scan` release evidence for the project bound here (extenshi project bind), bound to this exact package')
|
|
157
|
+
.option('--evidence-browser <list>', 'Store(s) the evidence is for: chrome,edge,firefox (default: read from the extenshi-packages.json next to the zip)')
|
|
128
158
|
.action(async (artifact, opts) => {
|
|
129
159
|
await runCommand('scan', async () => {
|
|
130
160
|
const config = await loadConfig(opts.apiUrl);
|
|
131
161
|
if (!config.apiKey) {
|
|
132
162
|
throw new Error(MISSING_KEY_HELP);
|
|
133
163
|
}
|
|
164
|
+
// Everything --evidence needs is settled before a credit is spent.
|
|
165
|
+
let evidence;
|
|
166
|
+
if (opts.evidence) {
|
|
167
|
+
assertEvidenceBinding();
|
|
168
|
+
const { digest } = await evidenceArtifact(artifact);
|
|
169
|
+
evidence = { digest, browsers: evidenceBrowsers(artifact, digest, opts.evidenceBrowser) };
|
|
170
|
+
}
|
|
134
171
|
// Auto-update check (fail-soft — never blocks scan)
|
|
135
172
|
await checkAutoUpdate();
|
|
136
173
|
let reportFormat;
|
|
@@ -161,7 +198,7 @@ program
|
|
|
161
198
|
// explicitly disabled (--no-open), and not in CI (no browser/display —
|
|
162
199
|
// the file is still written so CI can archive it as an artifact).
|
|
163
200
|
const openReport = format === 'html' && (opts.open ?? true) && !process.env.CI;
|
|
164
|
-
await runScan({
|
|
201
|
+
const body = await runScan({
|
|
165
202
|
artifactPath: artifact,
|
|
166
203
|
apiKey: config.apiKey,
|
|
167
204
|
apiUrl: config.apiUrl,
|
|
@@ -180,6 +217,8 @@ program
|
|
|
180
217
|
bffUrl: config.bffUrl,
|
|
181
218
|
listingPath: opts.listing,
|
|
182
219
|
});
|
|
220
|
+
if (evidence)
|
|
221
|
+
await recordAutoEvidence(scanEvidence({ artifactDigest: evidence.digest, browsers: evidence.browsers, body }));
|
|
183
222
|
});
|
|
184
223
|
});
|
|
185
224
|
program
|
|
@@ -191,6 +230,13 @@ program
|
|
|
191
230
|
.option('--stores <list>', 'Comma-separated stores: chrome,firefox,edge (default: every store with complete credentials)')
|
|
192
231
|
.option('--firefox-artifact <path>', 'Separate .xpi for Firefox (default: the main artifact)')
|
|
193
232
|
.option('--release-notes <text>', 'Release notes passed to stores that accept them')
|
|
233
|
+
.option('--listing <file>', 'Firefox: AMO listing (JSON, or the AMO.md `generate-listing --store firefox` writes, placeholders filled); ' +
|
|
234
|
+
'creates the add-on on first submission or edits its listing')
|
|
235
|
+
.option('--source <archive>', 'Firefox: reviewer source archive (.zip/.tar.gz) for the new version (needs --listing)')
|
|
236
|
+
.option('--screenshots <dir>', 'Firefox: AMO previews in file-name order: an image folder or `extenshi screenshots` output (its firefox/<locale>/ set); needs --listing')
|
|
237
|
+
.option('--allow-new-addon', 'Firefox: allow creating a NEW AMO add-on, whose id is permanent (in a bound project the owner-decided amo.addonId governs this)')
|
|
238
|
+
.option('--addon-id <id>', 'Firefox: the AMO add-on id ({GUID} or name@domain), stated explicitly; required with --allow-new-addon outside a bound project')
|
|
239
|
+
.option('--replace-screenshots', 'Firefox: replace the previews of an existing AMO add-on')
|
|
194
240
|
.option('--validate', 'Only validate store credentials, publish nothing')
|
|
195
241
|
.option('--no-wait', 'Edge: return after commit without polling the operation to a terminal status')
|
|
196
242
|
.option('--extension-id <id>', 'Numeric catalog ID — checks publish-beta access against this specific extension')
|
|
@@ -227,6 +273,12 @@ program
|
|
|
227
273
|
bffUrl: config.bffUrl,
|
|
228
274
|
apiKey: config.apiKey,
|
|
229
275
|
extensionId,
|
|
276
|
+
listingPath: opts.listing,
|
|
277
|
+
sourcePath: opts.source,
|
|
278
|
+
screenshotsDir: opts.screenshots,
|
|
279
|
+
allowNewAddon: opts.allowNewAddon ?? false,
|
|
280
|
+
addonId: opts.addonId,
|
|
281
|
+
replaceScreenshots: opts.replaceScreenshots ?? false,
|
|
230
282
|
});
|
|
231
283
|
});
|
|
232
284
|
});
|
|
@@ -245,8 +297,20 @@ program
|
|
|
245
297
|
'The package already supplies the localized name and short description; this adds the full description, ' +
|
|
246
298
|
'which exists only in the developer dashboard.')
|
|
247
299
|
.option('--json', 'Output raw JSON findings')
|
|
300
|
+
.option('--evidence', 'Record `permissions` evidence (bound to the packaged manifest) and, with --listing, `listing` evidence per locale ' +
|
|
301
|
+
'(bound to that file — pass the same file to release prepare --listing) for the project bound here')
|
|
302
|
+
.option('--evidence-browser <list>', 'Store(s) the evidence is for: chrome,edge,firefox (default: --store, else the extenshi-packages.json next to the zip)')
|
|
248
303
|
.action(async (artifact, opts) => {
|
|
249
304
|
await runCommand('review-risk', async () => {
|
|
305
|
+
let evidence;
|
|
306
|
+
if (opts.evidence) {
|
|
307
|
+
assertEvidenceBinding();
|
|
308
|
+
const { digest } = await evidenceArtifact(artifact);
|
|
309
|
+
evidence = {
|
|
310
|
+
digest,
|
|
311
|
+
browsers: evidenceBrowsers(artifact, digest, opts.evidenceBrowser ?? opts.store),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
250
314
|
// Default to CHROME — this tool is Chrome-Web-Store-centric, and pinning
|
|
251
315
|
// the store avoids diffing against another store's snapshot.
|
|
252
316
|
const storeMap = {
|
|
@@ -262,7 +326,7 @@ program
|
|
|
262
326
|
// manifest diff only uses the key if the catalog read path is gated.
|
|
263
327
|
const config = await loadConfig();
|
|
264
328
|
await checkAutoUpdate();
|
|
265
|
-
await runReviewRisk({
|
|
329
|
+
const result = await runReviewRisk({
|
|
266
330
|
artifactPath: artifact,
|
|
267
331
|
extensionId: opts.extensionId,
|
|
268
332
|
bffUrl: (opts.catalogUrl ?? 'https://bff.extenshi.io').replace(/\/$/, ''),
|
|
@@ -271,6 +335,27 @@ program
|
|
|
271
335
|
listingPath: opts.listing,
|
|
272
336
|
outputJson: opts.json ?? false,
|
|
273
337
|
});
|
|
338
|
+
if (evidence) {
|
|
339
|
+
const listingFindings = runListingOnlyChecks(result.listing);
|
|
340
|
+
const records = permissionsEvidence({
|
|
341
|
+
artifactDigest: evidence.digest,
|
|
342
|
+
manifest: result.manifest,
|
|
343
|
+
browsers: evidence.browsers,
|
|
344
|
+
findings: result.summary.findings,
|
|
345
|
+
listingCheckIds: new Set(listingFindings.map((f) => f.check)),
|
|
346
|
+
});
|
|
347
|
+
if (opts.listing)
|
|
348
|
+
records.push(...listingEvidence({
|
|
349
|
+
artifactDigest: evidence.digest,
|
|
350
|
+
listingBytes: await readFile(opts.listing),
|
|
351
|
+
listing: result.listing,
|
|
352
|
+
browsers: evidence.browsers,
|
|
353
|
+
listingFindings,
|
|
354
|
+
}));
|
|
355
|
+
else
|
|
356
|
+
process.stderr.write('note: no listing evidence — it binds to a listing file; pass --listing <file> (the one you give release prepare --listing)\n');
|
|
357
|
+
await recordAutoEvidence(records);
|
|
358
|
+
}
|
|
274
359
|
});
|
|
275
360
|
});
|
|
276
361
|
const localize = program
|
|
@@ -322,21 +407,140 @@ localize
|
|
|
322
407
|
});
|
|
323
408
|
program
|
|
324
409
|
.command('generate-listing <artifact>')
|
|
325
|
-
.description('Generate
|
|
410
|
+
.description('Generate a store listing draft from a packaged (.zip/.crx/.xpi) or unpacked extension: ' +
|
|
326
411
|
'listing copy, permission justifications, single purpose and privacy-practices questions. ' +
|
|
327
412
|
'Assembles the existing justification and privacy-disclosure checklists — does not write ' +
|
|
328
|
-
'anything into
|
|
329
|
-
|
|
413
|
+
'anything into any store dashboard. --store picks CHROMEWEBSTORE.md / EDGE.md / AMO.md ' +
|
|
414
|
+
'(default chrome); all three are the same listing draft `review-risk --listing` accepts.')
|
|
415
|
+
.option('--store <store>', 'Which store to draft for: chrome (default), edge, or firefox', 'chrome')
|
|
416
|
+
.option('--output <path>', 'Where to write the markdown (default: the per-store filename in the current directory)')
|
|
330
417
|
.option('--listing <file>', 'JSON file with your store listing text (name / shortDescription / description). ' +
|
|
331
418
|
'Overlays the package so the generated markdown can include the full dashboard description.')
|
|
419
|
+
.option('--evidence', 'After writing the file, run the listing rules over it and record `listing` evidence per locale, bound to the ' +
|
|
420
|
+
'generated file (pass it to release prepare --listing). Needs a packaged zip, not a folder.')
|
|
421
|
+
.option('--evidence-browser <list>', 'Store(s) the evidence is for: chrome,edge,firefox (default: the --store the draft is written for)')
|
|
332
422
|
.action(async (artifact, opts) => {
|
|
333
423
|
await runCommand('generate-listing', async () => {
|
|
424
|
+
const store = (opts.store ?? 'chrome').trim().toLowerCase();
|
|
425
|
+
if (store !== 'chrome' && store !== 'edge' && store !== 'firefox') {
|
|
426
|
+
throw new Error(`--store must be chrome, edge or firefox (got "${opts.store}")`);
|
|
427
|
+
}
|
|
428
|
+
// The draft is one store's listing, so its evidence is that store's by default.
|
|
429
|
+
let evidence;
|
|
430
|
+
if (opts.evidence) {
|
|
431
|
+
assertEvidenceBinding();
|
|
432
|
+
const { bytes, digest } = await evidenceArtifact(artifact);
|
|
433
|
+
evidence = {
|
|
434
|
+
bytes,
|
|
435
|
+
digest,
|
|
436
|
+
browsers: evidenceBrowsers(artifact, digest, opts.evidenceBrowser ?? store),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
334
439
|
await checkAutoUpdate();
|
|
335
|
-
await runGenerateListing({
|
|
440
|
+
const outputPath = await runGenerateListing({
|
|
336
441
|
artifactPath: artifact,
|
|
337
442
|
outputPath: opts.output,
|
|
338
443
|
listingPath: opts.listing,
|
|
444
|
+
store,
|
|
445
|
+
});
|
|
446
|
+
if (evidence)
|
|
447
|
+
await recordAutoEvidence(await generatedListingEvidence({
|
|
448
|
+
artifactBytes: evidence.bytes,
|
|
449
|
+
artifactDigest: evidence.digest,
|
|
450
|
+
draftPath: outputPath,
|
|
451
|
+
browsers: evidence.browsers,
|
|
452
|
+
}));
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
program
|
|
456
|
+
.command('package [dir]')
|
|
457
|
+
.description('Build one reproducible store zip per browser from an unpacked build directory: sorted entries, fixed ' +
|
|
458
|
+
'timestamps, no file modes or extra metadata — the same bytes on every machine and timezone. Prints the ' +
|
|
459
|
+
'sha256 of each zip and writes extenshi-packages.json, which release prepare, test smoke and evidence ' +
|
|
460
|
+
'ingest accept directly. Never modifies the source directory. Free, offline.')
|
|
461
|
+
.option('--browser <list>', 'Comma-separated: chrome,edge,firefox (default: chrome)')
|
|
462
|
+
.option('--chrome-dir <dir>', 'Build directory for Chrome (overrides [dir])')
|
|
463
|
+
.option('--edge-dir <dir>', 'Build directory for Edge (overrides [dir])')
|
|
464
|
+
.option('--firefox-dir <dir>', 'Build directory for Firefox (overrides [dir])')
|
|
465
|
+
.option('--out <dir>', 'Output directory (default: .extenshi/packages — ignored by the dirty-tree digest release prepare records)')
|
|
466
|
+
.option('--exclude <patterns...>', 'Leave files out (gitignore-style globs, comma-separated or repeated). Dot-entries and node_modules are always left out.')
|
|
467
|
+
.option('--gecko-id <id>', 'Firefox: write this add-on ID into the PACKAGED manifest. A missing or placeholder ID is refused otherwise.')
|
|
468
|
+
.option('--fix-war', 'Replace wildcard web_accessible_resources with the explicit list of matching files, in the packaged copy only')
|
|
469
|
+
.option('--json', 'Print the packages manifest as JSON')
|
|
470
|
+
.action(async (dir, opts) => {
|
|
471
|
+
await runCommand('package', async () => {
|
|
472
|
+
runPackage({
|
|
473
|
+
dir,
|
|
474
|
+
dirs: { chrome: opts.chromeDir, edge: opts.edgeDir, firefox: opts.firefoxDir },
|
|
475
|
+
browsers: parsePackageBrowsers(opts.browser),
|
|
476
|
+
outDir: opts.out ?? '.extenshi/packages',
|
|
477
|
+
exclude: opts.exclude,
|
|
478
|
+
geckoId: opts.geckoId,
|
|
479
|
+
fixWar: opts.fixWar ?? false,
|
|
480
|
+
json: opts.json ?? false,
|
|
481
|
+
cliVersion: pkg.version,
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
const test = program.command('test').description('Test packaged extensions in real browsers');
|
|
486
|
+
test
|
|
487
|
+
.command('smoke <package>')
|
|
488
|
+
.description('Install the EXACT package (a zip, or extenshi-packages.json + --browser) in a real browser and check that it ' +
|
|
489
|
+
'loads, its service worker/background starts, the browser reports no manifest errors, and its popup/options/' +
|
|
490
|
+
'side-panel pages open without console errors. chromium/edge use Playwright (install it in your project: ' +
|
|
491
|
+
'npm i -D playwright && npx playwright install chromium); firefox needs only a Firefox binary. Exit 1 on failure.')
|
|
492
|
+
.requiredOption('--browser <browser>', 'chromium (alias chrome) | edge | firefox')
|
|
493
|
+
.option('--json', 'Print the result as JSON')
|
|
494
|
+
.option('--timeout <ms>', 'Budget per wait (service worker start, page load)', '15000')
|
|
495
|
+
.option('--headed', 'Show the browser window')
|
|
496
|
+
.option('--firefox-bin <path>', 'Firefox binary (default: FIREFOX_BIN, standard install locations, PATH)')
|
|
497
|
+
.option('--browser-path <path>', 'Chromium/Edge executable instead of the Playwright channel default')
|
|
498
|
+
.option('--evidence', 'Record the result as `installed` release evidence for the project bound here (extenshi project bind)')
|
|
499
|
+
.action(async (packagePath, opts) => {
|
|
500
|
+
await runCommand('test smoke', async () => {
|
|
501
|
+
const engine = parseSmokeEngine(opts.browser);
|
|
502
|
+
if (opts.evidence)
|
|
503
|
+
assertEvidenceBinding();
|
|
504
|
+
const timeoutMs = Number.parseInt(opts.timeout, 10);
|
|
505
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1000)
|
|
506
|
+
throw new InvalidInputError('--timeout must be ≥ 1000 ms');
|
|
507
|
+
const { artifactPath } = resolvePackagedArtifact(packagePath, ENGINE_BROWSER[engine]);
|
|
508
|
+
const result = await runSmoke({
|
|
509
|
+
artifactPath,
|
|
510
|
+
engine,
|
|
511
|
+
timeoutMs,
|
|
512
|
+
headed: opts.headed ?? false,
|
|
513
|
+
firefoxBinary: opts.firefoxBin,
|
|
514
|
+
browserPath: opts.browserPath,
|
|
339
515
|
});
|
|
516
|
+
process.stdout.write(opts.json ? `${JSON.stringify(result, null, 2)}\n` : `${renderSmoke(result)}\n`);
|
|
517
|
+
if (opts.evidence)
|
|
518
|
+
await recordAutoEvidence(smokeEvidence({ browser: ENGINE_BROWSER[engine], result }));
|
|
519
|
+
if (result.status === 'failed')
|
|
520
|
+
process.exitCode = 1;
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
program
|
|
524
|
+
.command('amo-source <project-dir>')
|
|
525
|
+
.description('Produce the AMO source-code archive + BUILD.md reviewer instructions for a Firefox add-on ' +
|
|
526
|
+
'project directory. Excludes node_modules/dist/.git and dotenv secrets (.env*); includes the ' +
|
|
527
|
+
'lockfile. Refuses on an uncommitted git working tree (the archive should match a real commit). ' +
|
|
528
|
+
'Flags vendored minified files with no reproducible-build provenance as blockers. Does not talk ' +
|
|
529
|
+
'to the AMO API — upload the archive by hand, or via the separate AMO API submission path.')
|
|
530
|
+
.option('--out <path>', 'Where to write the archive (default: amo-source.zip in the current directory)')
|
|
531
|
+
.action(async (projectDir, opts) => {
|
|
532
|
+
await runCommand('amo-source', async () => {
|
|
533
|
+
await checkAutoUpdate();
|
|
534
|
+
const result = await runAmoSource({ projectDir, outputPath: opts.out });
|
|
535
|
+
console.log(`Wrote ${result.zipPath} (${result.fileCount} files)`);
|
|
536
|
+
console.log(`Wrote ${result.buildMdPath}`);
|
|
537
|
+
if (result.blockers.length > 0) {
|
|
538
|
+
console.log(`\n${result.blockers.length} file(s) need a source-code submission before this can go to AMO:`);
|
|
539
|
+
for (const f of result.blockers)
|
|
540
|
+
console.log(` - ${f.path}`);
|
|
541
|
+
console.log('See BUILD.md for what to do about each one.');
|
|
542
|
+
process.exitCode = 1;
|
|
543
|
+
}
|
|
340
544
|
});
|
|
341
545
|
});
|
|
342
546
|
program
|
|
@@ -399,6 +603,58 @@ icon
|
|
|
399
603
|
await runIconPreview(iconPath, opts, pkg.version);
|
|
400
604
|
});
|
|
401
605
|
});
|
|
606
|
+
icon
|
|
607
|
+
.command('store-assets <source>')
|
|
608
|
+
.description('Render the store LISTING artwork from one .svg / .png source: Chrome Web Store icon 128 (96 px artwork + ' +
|
|
609
|
+
'16 px padding), 440×280 promo tile and 1400×560 marquee; Edge Add-ons 300×300 logo and 440×280 / ' +
|
|
610
|
+
'1400×560 tiles; AMO 128 / 64 / 32 icons. Offline (WebAssembly renderer), no API key.')
|
|
611
|
+
.option('--store <stores>', 'Comma-separated: chrome, edge, firefox (default: all)')
|
|
612
|
+
.option('--out <dir>', 'Output directory (default: ./store-assets)')
|
|
613
|
+
.option('--background <color>', 'Promo tile / marquee background (default: a light tint of the icon colour)')
|
|
614
|
+
.option('--icon-background <color>', 'Fill behind icons and the Edge logo (default: transparent)')
|
|
615
|
+
.option('--promo <file>', 'Full-bleed artwork (.svg/.png) for the 440×280 tiles instead of composing the icon')
|
|
616
|
+
.option('--marquee <file>', 'Full-bleed artwork (.svg/.png) for the 1400×560 tiles instead of composing the icon')
|
|
617
|
+
.option('--font <files...>', 'Font files for text in SVG sources (system fonts are matched by name otherwise)')
|
|
618
|
+
.option('--json', 'Print the result as JSON')
|
|
619
|
+
.action(async (source, opts) => {
|
|
620
|
+
await runCommand('icon store-assets', async () => {
|
|
621
|
+
await runStoreAssets(source, opts);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
program
|
|
625
|
+
.command('screenshots <extension>')
|
|
626
|
+
.description('Store screenshots without a human: load an unpacked build (or .zip) in Chromium, run the declarative ' +
|
|
627
|
+
'scenes in --scenes per locale (the UI language is switched and verified through chrome.i18n), and ' +
|
|
628
|
+
'write each store size — Chrome/Edge/AMO 1280×800 (or --small) — as <out>/<store>/<locale>/NN-<scene>-WxH.png ' +
|
|
629
|
+
'plus screenshots.json. Needs Playwright with Chromium in the project (npm i -D playwright && npx playwright ' +
|
|
630
|
+
'install chromium). Scenes format: https://docs.extenshi.io/developers/cli#extenshi-screenshots')
|
|
631
|
+
.requiredOption('--scenes <scenes.json>', 'Declarative scenes file')
|
|
632
|
+
.option('--locales <codes>', 'Comma-separated _locales codes (default: every locale the extension ships)')
|
|
633
|
+
.option('--store <stores>', 'Comma-separated: chrome, edge, firefox (default: all)')
|
|
634
|
+
.option('--out <dir>', 'Output directory (default: ./store-screenshots)')
|
|
635
|
+
.option('--small', "Each store's smaller size: Chrome 640×400, Edge 640×480, AMO 640×400")
|
|
636
|
+
.option('--headed', 'Show the browser window (debugging scenes)')
|
|
637
|
+
.option('--json', 'Print the result as JSON')
|
|
638
|
+
.action(async (extension, opts) => {
|
|
639
|
+
await runCommand('screenshots', async () => {
|
|
640
|
+
await runScreenshotsCommand(extension, opts);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
program
|
|
644
|
+
.command('media')
|
|
645
|
+
.description("The project's PUBLIC media store (logo, screenshots) used by hosted pages")
|
|
646
|
+
.command('upload <files...>')
|
|
647
|
+
.description("Upload PNG/JPEG/WebP images (≤2 MB each; .svg is rasterized to PNG locally) to the project's public media " +
|
|
648
|
+
'store and print their stable URLs — the same store Dojo uploads use, accepted by `page publish` as ' +
|
|
649
|
+
'screenshots[].url / logoUrl. The server strips metadata and enforces a per-project quota. Requires an ' +
|
|
650
|
+
'API key (extenshi login) with hosted.write.')
|
|
651
|
+
.requiredOption('--project <id>', 'Dojo project ID (UUID)')
|
|
652
|
+
.option('--svg-width <px>', 'Width to rasterize .svg files at (default 1024)')
|
|
653
|
+
.action(async (files, opts) => {
|
|
654
|
+
await runCommand('media upload', async () => {
|
|
655
|
+
await runMediaUpload(files, opts);
|
|
656
|
+
});
|
|
657
|
+
});
|
|
402
658
|
const page = program
|
|
403
659
|
.command('page')
|
|
404
660
|
.description('Landing page (homepage) for your extension: generate static HTML offline, register its URL');
|
|
@@ -438,6 +694,7 @@ page
|
|
|
438
694
|
.requiredOption('--config <landing.json>', 'Landing config JSON')
|
|
439
695
|
.option('--logo <file>', 'Logo: .svg or .png/.jpg/.webp/.gif (≤256 KiB)')
|
|
440
696
|
.option('--no-homepage', 'Do not register the URL as the project homepage')
|
|
697
|
+
.option('--upload-local', "Upload local screenshot / logoUrl paths (relative to the config file) to the project's public media store first")
|
|
441
698
|
.action(async (opts) => {
|
|
442
699
|
await runCommand('page publish', async () => {
|
|
443
700
|
await runPagePublish(opts);
|