@extension.dev/deploy 0.1.1 → 0.2.1
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 +30 -0
- package/dist/__tests__/init.test.d.ts +2 -0
- package/dist/__tests__/init.test.d.ts.map +1 -0
- package/dist/module.js +363 -1
- package/dist/module.mjs +362 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/config.d.ts +10 -2
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/init-io.d.ts +21 -0
- package/dist/src/init-io.d.ts.map +1 -0
- package/dist/src/init.d.ts +75 -0
- package/dist/src/init.d.ts.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,8 +22,38 @@ Deploy browser extensions to Chrome Web Store, Firefox AMO, and Edge Partner Cen
|
|
|
22
22
|
npm install @extension.dev/deploy
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
If you're setting up a new project, run the interactive wizard. It walks you
|
|
28
|
+
through each store's credential acquisition (including auto-generating a
|
|
29
|
+
Chrome refresh token from your OAuth client), runs a verification check
|
|
30
|
+
against each store's API, and writes everything to a `.env.submit` file:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx extension-deploy init
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Non-interactive use (e.g. CI setup scripts):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx extension-deploy init \
|
|
40
|
+
--init-stores chrome,firefox \
|
|
41
|
+
--init-output config/.env.submit \
|
|
42
|
+
--init-skip-verify \
|
|
43
|
+
--init-force
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
After `init` has written `.env.submit`, a full submission is a one-liner:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx extension-deploy --dry-run # validate creds and ZIPs
|
|
50
|
+
npx extension-deploy # actually submit
|
|
51
|
+
```
|
|
52
|
+
|
|
25
53
|
## CLI usage
|
|
26
54
|
|
|
55
|
+
If you'd rather skip the wizard and pass credentials directly:
|
|
56
|
+
|
|
27
57
|
```bash
|
|
28
58
|
npx extension-deploy \
|
|
29
59
|
--chrome-zip dist/chrome.zip \
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.test.d.ts","sourceRoot":"","sources":["../../__tests__/init.test.ts"],"names":[],"mappings":""}
|
package/dist/module.js
CHANGED
|
@@ -942,12 +942,329 @@ async function runWatchLoop(_store, probe, options) {
|
|
|
942
942
|
await delay(pollInterval);
|
|
943
943
|
}
|
|
944
944
|
}
|
|
945
|
+
const STORE_CHOICES = [
|
|
946
|
+
{
|
|
947
|
+
value: "chrome",
|
|
948
|
+
label: "Chrome Web Store"
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
value: "firefox",
|
|
952
|
+
label: "Firefox AMO (addons.mozilla.org)"
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
value: "edge",
|
|
956
|
+
label: "Edge Add-ons (Partner Center)"
|
|
957
|
+
}
|
|
958
|
+
];
|
|
959
|
+
const CHROME_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
960
|
+
const CHROME_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
961
|
+
const CHROME_OOB_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
|
|
962
|
+
const CHROME_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
|
963
|
+
async function runInit(deps, options = {}) {
|
|
964
|
+
const { io } = deps;
|
|
965
|
+
io.log("");
|
|
966
|
+
io.log("extension-deploy init \u2014 set up store credentials");
|
|
967
|
+
io.log("");
|
|
968
|
+
io.log("This wizard walks you through creating credentials for each store and");
|
|
969
|
+
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
970
|
+
io.log("");
|
|
971
|
+
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
972
|
+
if (0 === stores.length) throw new Error("init: at least one store must be selected. Use --init-stores chrome,firefox,edge or make a selection.");
|
|
973
|
+
const envLines = [
|
|
974
|
+
"# Generated by `extension-deploy init`.",
|
|
975
|
+
"# Do not commit secrets to source control.",
|
|
976
|
+
""
|
|
977
|
+
];
|
|
978
|
+
const verified = {};
|
|
979
|
+
for (const store of stores){
|
|
980
|
+
io.log("");
|
|
981
|
+
io.log(`=== ${labelFor(store)} ===`);
|
|
982
|
+
io.log("");
|
|
983
|
+
if ("chrome" === store) {
|
|
984
|
+
const creds = await collectChromeCredentials(deps);
|
|
985
|
+
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
986
|
+
if (!options.skipVerify) {
|
|
987
|
+
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
988
|
+
verified.chrome = normalizeVerify(await verify(creds));
|
|
989
|
+
logVerify(io, "chrome", verified.chrome);
|
|
990
|
+
}
|
|
991
|
+
} else if ("firefox" === store) {
|
|
992
|
+
const creds = await collectFirefoxCredentials(deps);
|
|
993
|
+
envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
|
|
994
|
+
if (!options.skipVerify) {
|
|
995
|
+
const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
|
|
996
|
+
verified.firefox = normalizeVerify(await verify({
|
|
997
|
+
jwtIssuer: creds.jwtIssuer,
|
|
998
|
+
jwtSecret: creds.jwtSecret,
|
|
999
|
+
addonId: creds.addonId
|
|
1000
|
+
}));
|
|
1001
|
+
logVerify(io, "firefox", verified.firefox);
|
|
1002
|
+
}
|
|
1003
|
+
} else {
|
|
1004
|
+
const creds = await collectEdgeCredentials(deps);
|
|
1005
|
+
envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
|
|
1006
|
+
if (!options.skipVerify) {
|
|
1007
|
+
const verify = deps.verifyEdge ?? verifyEdgeCredentials;
|
|
1008
|
+
verified.edge = normalizeVerify(await verify({
|
|
1009
|
+
clientId: creds.clientId,
|
|
1010
|
+
apiKey: creds.apiKey,
|
|
1011
|
+
productId: creds.productId
|
|
1012
|
+
}));
|
|
1013
|
+
logVerify(io, "edge", verified.edge);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const envContent = envLines.join("\n");
|
|
1018
|
+
const envPath = resolveEnvPath(deps.cwd, options.outputPath);
|
|
1019
|
+
const exists = await deps.fileExists(envPath);
|
|
1020
|
+
let wrote = false;
|
|
1021
|
+
if (exists && !options.force) {
|
|
1022
|
+
const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
|
|
1023
|
+
if (overwrite) {
|
|
1024
|
+
await deps.writeFile(envPath, envContent);
|
|
1025
|
+
wrote = true;
|
|
1026
|
+
} else {
|
|
1027
|
+
io.log("");
|
|
1028
|
+
io.log(`Skipped writing ${envPath}. Credential block:`);
|
|
1029
|
+
io.log("");
|
|
1030
|
+
io.log(envContent);
|
|
1031
|
+
}
|
|
1032
|
+
} else {
|
|
1033
|
+
await deps.writeFile(envPath, envContent);
|
|
1034
|
+
wrote = true;
|
|
1035
|
+
}
|
|
1036
|
+
if (wrote) {
|
|
1037
|
+
io.log("");
|
|
1038
|
+
io.log(`Wrote ${envPath}`);
|
|
1039
|
+
io.log("");
|
|
1040
|
+
io.log("Next steps:");
|
|
1041
|
+
io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
|
|
1042
|
+
io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
|
|
1043
|
+
io.log("");
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
stores,
|
|
1047
|
+
envPath,
|
|
1048
|
+
envContent,
|
|
1049
|
+
verified,
|
|
1050
|
+
wrote
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
async function collectChromeCredentials(deps) {
|
|
1054
|
+
const { io } = deps;
|
|
1055
|
+
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1056
|
+
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1057
|
+
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1058
|
+
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1059
|
+
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1060
|
+
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1061
|
+
io.log("");
|
|
1062
|
+
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1063
|
+
io.log("generating a refresh token automatically.");
|
|
1064
|
+
io.log("");
|
|
1065
|
+
const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
|
|
1066
|
+
const clientId = await requireValue(io, "OAuth Client ID");
|
|
1067
|
+
const clientSecret = await requireValue(io, "OAuth Client Secret", void 0, {
|
|
1068
|
+
secret: true
|
|
1069
|
+
});
|
|
1070
|
+
const refreshToken = await obtainChromeRefreshToken(deps, clientId, clientSecret);
|
|
1071
|
+
return {
|
|
1072
|
+
extensionId,
|
|
1073
|
+
clientId,
|
|
1074
|
+
clientSecret,
|
|
1075
|
+
refreshToken
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
function buildChromeAuthUrl(clientId) {
|
|
1079
|
+
const params = new URLSearchParams({
|
|
1080
|
+
response_type: "code",
|
|
1081
|
+
client_id: clientId,
|
|
1082
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1083
|
+
scope: CHROME_SCOPE,
|
|
1084
|
+
access_type: "offline",
|
|
1085
|
+
prompt: "consent"
|
|
1086
|
+
});
|
|
1087
|
+
return `${CHROME_OAUTH_AUTH_URL}?${params.toString()}`;
|
|
1088
|
+
}
|
|
1089
|
+
async function exchangeChromeAuthCode(input) {
|
|
1090
|
+
const fetchFn = input.fetchImpl ?? fetch;
|
|
1091
|
+
const body = new URLSearchParams({
|
|
1092
|
+
code: input.authCode,
|
|
1093
|
+
client_id: input.clientId,
|
|
1094
|
+
client_secret: input.clientSecret,
|
|
1095
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1096
|
+
grant_type: "authorization_code"
|
|
1097
|
+
});
|
|
1098
|
+
const res = await fetchFn(CHROME_OAUTH_TOKEN_URL, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: {
|
|
1101
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1102
|
+
},
|
|
1103
|
+
body: body.toString()
|
|
1104
|
+
});
|
|
1105
|
+
if (!res.ok) {
|
|
1106
|
+
const text = await res.text().catch(()=>"");
|
|
1107
|
+
throw new Error(`Chrome token exchange failed (HTTP ${res.status}): ${text || res.statusText}`);
|
|
1108
|
+
}
|
|
1109
|
+
const json = await res.json();
|
|
1110
|
+
if (!json.refresh_token) throw new Error(`Chrome token exchange returned no refresh_token${json.error ? `: ${json.error} \u{2014} ${json.error_description ?? ""}` : ""}. This usually means the authorization code was already used or expired; try the init wizard again.`);
|
|
1111
|
+
return json.refresh_token;
|
|
1112
|
+
}
|
|
1113
|
+
async function obtainChromeRefreshToken(deps, clientId, clientSecret) {
|
|
1114
|
+
const { io } = deps;
|
|
1115
|
+
const authUrl = buildChromeAuthUrl(clientId);
|
|
1116
|
+
io.log("");
|
|
1117
|
+
io.log("Open the URL below in your browser, approve the scope, and paste the");
|
|
1118
|
+
io.log("authorization code shown on the page:");
|
|
1119
|
+
io.log("");
|
|
1120
|
+
io.log(` ${authUrl}`);
|
|
1121
|
+
io.log("");
|
|
1122
|
+
const authCode = await requireValue(io, "Authorization code", void 0, {
|
|
1123
|
+
secret: true
|
|
1124
|
+
});
|
|
1125
|
+
return exchangeChromeAuthCode({
|
|
1126
|
+
clientId,
|
|
1127
|
+
clientSecret,
|
|
1128
|
+
authCode,
|
|
1129
|
+
fetchImpl: deps.fetchImpl
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
async function collectFirefoxCredentials(deps) {
|
|
1133
|
+
const { io } = deps;
|
|
1134
|
+
io.log("Firefox AMO needs JWT API credentials:");
|
|
1135
|
+
io.log(" 1. Sign in at https://addons.mozilla.org");
|
|
1136
|
+
io.log(" 2. Create API credentials at https://addons.mozilla.org/developers/addon/api/key/");
|
|
1137
|
+
io.log(" 3. Copy the JWT issuer and JWT secret (the secret is only shown once).");
|
|
1138
|
+
io.log("");
|
|
1139
|
+
const addonId = await requireValue(io, "Firefox addon ID", "({uuid} GUID or email-style ID from your addon page)");
|
|
1140
|
+
const jwtIssuer = await requireValue(io, "JWT issuer");
|
|
1141
|
+
const jwtSecret = await requireValue(io, "JWT secret", void 0, {
|
|
1142
|
+
secret: true
|
|
1143
|
+
});
|
|
1144
|
+
return {
|
|
1145
|
+
addonId,
|
|
1146
|
+
jwtIssuer,
|
|
1147
|
+
jwtSecret
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
async function collectEdgeCredentials(deps) {
|
|
1151
|
+
const { io } = deps;
|
|
1152
|
+
io.log("Edge Partner Center needs the v1.1 API credentials:");
|
|
1153
|
+
io.log(" 1. Open the Partner Center dashboard: https://partner.microsoft.com/dashboard/microsoftedge/overview");
|
|
1154
|
+
io.log(" 2. Under your extension, go to Settings \u2192 API access \u2192 API v1.1 and generate credentials.");
|
|
1155
|
+
io.log(" 3. Copy the Client ID, API key, and Product ID.");
|
|
1156
|
+
io.log("");
|
|
1157
|
+
const productId = await requireValue(io, "Edge product ID");
|
|
1158
|
+
const clientId = await requireValue(io, "Edge client ID");
|
|
1159
|
+
const apiKey = await requireValue(io, "Edge API key", void 0, {
|
|
1160
|
+
secret: true
|
|
1161
|
+
});
|
|
1162
|
+
return {
|
|
1163
|
+
productId,
|
|
1164
|
+
clientId,
|
|
1165
|
+
apiKey
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
function labelFor(store) {
|
|
1169
|
+
var _STORE_CHOICES_find;
|
|
1170
|
+
return (null == (_STORE_CHOICES_find = STORE_CHOICES.find((c)=>c.value === store)) ? void 0 : _STORE_CHOICES_find.label) ?? String(store);
|
|
1171
|
+
}
|
|
1172
|
+
function normalizeVerify(result) {
|
|
1173
|
+
return {
|
|
1174
|
+
ok: result.ok,
|
|
1175
|
+
message: result.message
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function logVerify(io, store, record) {
|
|
1179
|
+
const marker = record.ok ? "[ok]" : "[fail]";
|
|
1180
|
+
io.log(` ${marker} verify ${store}: ${record.message}`);
|
|
1181
|
+
}
|
|
1182
|
+
function resolveEnvPath(cwd, outputPath) {
|
|
1183
|
+
const target = outputPath ?? ".env.submit";
|
|
1184
|
+
return external_node_path_default().isAbsolute(target) ? target : external_node_path_default().join(cwd, target);
|
|
1185
|
+
}
|
|
1186
|
+
async function requireValue(io, label, hint, opts) {
|
|
1187
|
+
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1188
|
+
for(let attempt = 0; attempt < 3; attempt++){
|
|
1189
|
+
const answer = (await io.prompt(question, opts)).trim();
|
|
1190
|
+
if (answer) return answer;
|
|
1191
|
+
io.log(` ${label} is required.`);
|
|
1192
|
+
}
|
|
1193
|
+
throw new Error(`init: ${label} is required.`);
|
|
1194
|
+
}
|
|
1195
|
+
const external_node_readline_namespaceObject = require("node:readline");
|
|
1196
|
+
var external_node_readline_default = /*#__PURE__*/ __webpack_require__.n(external_node_readline_namespaceObject);
|
|
1197
|
+
function createStdinIO() {
|
|
1198
|
+
const rl = external_node_readline_default().createInterface({
|
|
1199
|
+
input: process.stdin,
|
|
1200
|
+
output: process.stdout,
|
|
1201
|
+
terminal: false
|
|
1202
|
+
});
|
|
1203
|
+
const lineIter = rl[Symbol.asyncIterator]();
|
|
1204
|
+
async function readLine() {
|
|
1205
|
+
const { value, done } = await lineIter.next();
|
|
1206
|
+
if (done) return "";
|
|
1207
|
+
return value;
|
|
1208
|
+
}
|
|
1209
|
+
function write(chunk) {
|
|
1210
|
+
process.stdout.write(chunk);
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
async prompt (question, opts) {
|
|
1214
|
+
const suffix = (null == opts ? void 0 : opts.default) ? ` [${opts.default}]` : "";
|
|
1215
|
+
const secretNote = (null == opts ? void 0 : opts.secret) ? " (input visible)" : "";
|
|
1216
|
+
write(`${question}${suffix}${secretNote} `);
|
|
1217
|
+
const answer = await readLine();
|
|
1218
|
+
if (0 === answer.length && (null == opts ? void 0 : opts.default)) return opts.default;
|
|
1219
|
+
return answer;
|
|
1220
|
+
},
|
|
1221
|
+
async confirm (question, defaultValue = false) {
|
|
1222
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
1223
|
+
write(`${question} ${suffix} `);
|
|
1224
|
+
const raw = (await readLine()).trim().toLowerCase();
|
|
1225
|
+
if ("" === raw) return defaultValue;
|
|
1226
|
+
return "y" === raw || "yes" === raw;
|
|
1227
|
+
},
|
|
1228
|
+
async multiselect (question, choices) {
|
|
1229
|
+
write(`${question}\n`);
|
|
1230
|
+
choices.forEach((choice, idx)=>{
|
|
1231
|
+
write(` ${idx + 1}) ${choice.label}\n`);
|
|
1232
|
+
});
|
|
1233
|
+
write(' Enter comma-separated numbers (e.g. 1,2 or "all"): ');
|
|
1234
|
+
const raw = (await readLine()).trim().toLowerCase();
|
|
1235
|
+
if ("all" === raw) return choices.map((c)=>c.value);
|
|
1236
|
+
const indices = raw.split(/[,\s]+/).map((token)=>Number.parseInt(token, 10)).filter((n)=>Number.isInteger(n) && n >= 1 && n <= choices.length);
|
|
1237
|
+
const unique = Array.from(new Set(indices));
|
|
1238
|
+
return unique.map((i)=>choices[i - 1].value);
|
|
1239
|
+
},
|
|
1240
|
+
log (message) {
|
|
1241
|
+
write(`${message}\n`);
|
|
1242
|
+
},
|
|
1243
|
+
dispose () {
|
|
1244
|
+
rl.close();
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
const stdinFs = {
|
|
1249
|
+
async writeFile (filePath, content) {
|
|
1250
|
+
await promises_default().writeFile(filePath, content, "utf-8");
|
|
1251
|
+
},
|
|
1252
|
+
async fileExists (filePath) {
|
|
1253
|
+
try {
|
|
1254
|
+
await promises_default().access(filePath);
|
|
1255
|
+
return true;
|
|
1256
|
+
} catch {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
945
1261
|
function usage() {
|
|
946
1262
|
return `
|
|
947
1263
|
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
948
1264
|
|
|
949
1265
|
USAGE
|
|
950
1266
|
extension-deploy [options] submit to one or more stores
|
|
1267
|
+
extension-deploy init [options] interactive credential setup wizard
|
|
951
1268
|
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
952
1269
|
extension-deploy watch --firefox [options]
|
|
953
1270
|
extension-deploy watch --edge [options]
|
|
@@ -988,6 +1305,12 @@ WATCH
|
|
|
988
1305
|
--watch-interval <seconds> Poll interval (default 60)
|
|
989
1306
|
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
990
1307
|
|
|
1308
|
+
INIT
|
|
1309
|
+
--init-stores <list> Comma-separated store list (skip picker)
|
|
1310
|
+
--init-output <path> Destination file (default .env.submit)
|
|
1311
|
+
--init-skip-verify Don't run verifyCredentials after collection
|
|
1312
|
+
--init-force Overwrite existing .env.submit without asking
|
|
1313
|
+
|
|
991
1314
|
ENVIRONMENT VARIABLES
|
|
992
1315
|
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
993
1316
|
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
@@ -999,7 +1322,9 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
999
1322
|
"dryRun",
|
|
1000
1323
|
"chromeReviewExemption",
|
|
1001
1324
|
"chromeSkipSubmitReview",
|
|
1002
|
-
"edgeSkipSubmitReview"
|
|
1325
|
+
"edgeSkipSubmitReview",
|
|
1326
|
+
"initSkipVerify",
|
|
1327
|
+
"initForce"
|
|
1003
1328
|
]);
|
|
1004
1329
|
const INT_FLAGS = new Set([
|
|
1005
1330
|
"chromeDeployPercentage",
|
|
@@ -1012,6 +1337,9 @@ function parseArgs(argv) {
|
|
|
1012
1337
|
if ("watch" === argv[0]) {
|
|
1013
1338
|
flags.command = "watch";
|
|
1014
1339
|
i = 1;
|
|
1340
|
+
} else if ("init" === argv[0]) {
|
|
1341
|
+
flags.command = "init";
|
|
1342
|
+
i = 1;
|
|
1015
1343
|
}
|
|
1016
1344
|
while(i < argv.length){
|
|
1017
1345
|
const arg = argv[i];
|
|
@@ -1086,6 +1414,7 @@ async function main() {
|
|
|
1086
1414
|
process.exit(0);
|
|
1087
1415
|
}
|
|
1088
1416
|
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1417
|
+
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1089
1418
|
const config = resolveConfig(flags);
|
|
1090
1419
|
try {
|
|
1091
1420
|
const result = await deploy(config);
|
|
@@ -1154,6 +1483,39 @@ function mustValue(flagName, value) {
|
|
|
1154
1483
|
if (!value) throw new Error(`Missing required flag: ${flagName}`);
|
|
1155
1484
|
return value;
|
|
1156
1485
|
}
|
|
1486
|
+
async function runInitCommand(flags) {
|
|
1487
|
+
const stores = parseInitStores(flags.initStores);
|
|
1488
|
+
const io = createStdinIO();
|
|
1489
|
+
try {
|
|
1490
|
+
await runInit({
|
|
1491
|
+
io,
|
|
1492
|
+
cwd: process.cwd(),
|
|
1493
|
+
writeFile: stdinFs.writeFile,
|
|
1494
|
+
fileExists: stdinFs.fileExists
|
|
1495
|
+
}, {
|
|
1496
|
+
stores,
|
|
1497
|
+
outputPath: flags.initOutput,
|
|
1498
|
+
skipVerify: flags.initSkipVerify,
|
|
1499
|
+
force: flags.initForce
|
|
1500
|
+
});
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
} finally{
|
|
1505
|
+
io.dispose();
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
function parseInitStores(raw) {
|
|
1509
|
+
if (!raw) return;
|
|
1510
|
+
const allowed = [
|
|
1511
|
+
"chrome",
|
|
1512
|
+
"firefox",
|
|
1513
|
+
"edge"
|
|
1514
|
+
];
|
|
1515
|
+
const parsed = raw.split(",").map((s)=>s.trim().toLowerCase()).filter((s)=>allowed.includes(s));
|
|
1516
|
+
if (0 === parsed.length) throw new Error(`--init-stores: expected a comma-separated list of chrome,firefox,edge (got "${raw}")`);
|
|
1517
|
+
return parsed;
|
|
1518
|
+
}
|
|
1157
1519
|
exports.chromeOptionsSchema = __webpack_exports__.chromeOptionsSchema;
|
|
1158
1520
|
exports.deploy = __webpack_exports__.deploy;
|
|
1159
1521
|
exports.deployConfigSchema = __webpack_exports__.deployConfigSchema;
|
package/dist/module.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import external_node_path_default from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import promises_default from "node:fs/promises";
|
|
5
5
|
import external_node_crypto_default from "node:crypto";
|
|
6
|
+
import external_node_readline_default from "node:readline";
|
|
6
7
|
const chromeOptionsSchema = z.object({
|
|
7
8
|
zip: z.string().min(1),
|
|
8
9
|
extensionId: z.string().min(1).trim(),
|
|
@@ -885,12 +886,327 @@ async function runWatchLoop(_store, probe, options) {
|
|
|
885
886
|
await delay(pollInterval);
|
|
886
887
|
}
|
|
887
888
|
}
|
|
889
|
+
const STORE_CHOICES = [
|
|
890
|
+
{
|
|
891
|
+
value: "chrome",
|
|
892
|
+
label: "Chrome Web Store"
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
value: "firefox",
|
|
896
|
+
label: "Firefox AMO (addons.mozilla.org)"
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
value: "edge",
|
|
900
|
+
label: "Edge Add-ons (Partner Center)"
|
|
901
|
+
}
|
|
902
|
+
];
|
|
903
|
+
const CHROME_OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
904
|
+
const CHROME_OAUTH_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
|
|
905
|
+
const CHROME_OOB_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
|
|
906
|
+
const CHROME_SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
|
907
|
+
async function runInit(deps, options = {}) {
|
|
908
|
+
const { io } = deps;
|
|
909
|
+
io.log("");
|
|
910
|
+
io.log("extension-deploy init \u2014 set up store credentials");
|
|
911
|
+
io.log("");
|
|
912
|
+
io.log("This wizard walks you through creating credentials for each store and");
|
|
913
|
+
io.log("writes them to a .env.submit file. Press Ctrl+C at any time to abort.");
|
|
914
|
+
io.log("");
|
|
915
|
+
const stores = options.stores ? options.stores : await io.multiselect("Which stores do you want to configure?", STORE_CHOICES);
|
|
916
|
+
if (0 === stores.length) throw new Error("init: at least one store must be selected. Use --init-stores chrome,firefox,edge or make a selection.");
|
|
917
|
+
const envLines = [
|
|
918
|
+
"# Generated by `extension-deploy init`.",
|
|
919
|
+
"# Do not commit secrets to source control.",
|
|
920
|
+
""
|
|
921
|
+
];
|
|
922
|
+
const verified = {};
|
|
923
|
+
for (const store of stores){
|
|
924
|
+
io.log("");
|
|
925
|
+
io.log(`=== ${labelFor(store)} ===`);
|
|
926
|
+
io.log("");
|
|
927
|
+
if ("chrome" === store) {
|
|
928
|
+
const creds = await collectChromeCredentials(deps);
|
|
929
|
+
envLines.push(`CHROME_EXTENSION_ID=${creds.extensionId}`, `CHROME_CLIENT_ID=${creds.clientId}`, `CHROME_CLIENT_SECRET=${creds.clientSecret}`, `CHROME_REFRESH_TOKEN=${creds.refreshToken}`, "");
|
|
930
|
+
if (!options.skipVerify) {
|
|
931
|
+
const verify = deps.verifyChrome ?? verifyChromeCredentials;
|
|
932
|
+
verified.chrome = normalizeVerify(await verify(creds));
|
|
933
|
+
logVerify(io, "chrome", verified.chrome);
|
|
934
|
+
}
|
|
935
|
+
} else if ("firefox" === store) {
|
|
936
|
+
const creds = await collectFirefoxCredentials(deps);
|
|
937
|
+
envLines.push(`FIREFOX_EXTENSION_ID=${creds.addonId}`, `FIREFOX_JWT_ISSUER=${creds.jwtIssuer}`, `FIREFOX_JWT_SECRET=${creds.jwtSecret}`, "");
|
|
938
|
+
if (!options.skipVerify) {
|
|
939
|
+
const verify = deps.verifyFirefox ?? verifyFirefoxCredentials;
|
|
940
|
+
verified.firefox = normalizeVerify(await verify({
|
|
941
|
+
jwtIssuer: creds.jwtIssuer,
|
|
942
|
+
jwtSecret: creds.jwtSecret,
|
|
943
|
+
addonId: creds.addonId
|
|
944
|
+
}));
|
|
945
|
+
logVerify(io, "firefox", verified.firefox);
|
|
946
|
+
}
|
|
947
|
+
} else {
|
|
948
|
+
const creds = await collectEdgeCredentials(deps);
|
|
949
|
+
envLines.push(`EDGE_PRODUCT_ID=${creds.productId}`, `EDGE_CLIENT_ID=${creds.clientId}`, `EDGE_API_KEY=${creds.apiKey}`, "");
|
|
950
|
+
if (!options.skipVerify) {
|
|
951
|
+
const verify = deps.verifyEdge ?? verifyEdgeCredentials;
|
|
952
|
+
verified.edge = normalizeVerify(await verify({
|
|
953
|
+
clientId: creds.clientId,
|
|
954
|
+
apiKey: creds.apiKey,
|
|
955
|
+
productId: creds.productId
|
|
956
|
+
}));
|
|
957
|
+
logVerify(io, "edge", verified.edge);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const envContent = envLines.join("\n");
|
|
962
|
+
const envPath = resolveEnvPath(deps.cwd, options.outputPath);
|
|
963
|
+
const exists = await deps.fileExists(envPath);
|
|
964
|
+
let wrote = false;
|
|
965
|
+
if (exists && !options.force) {
|
|
966
|
+
const overwrite = await deps.io.confirm(`${envPath} already exists. Overwrite?`, false);
|
|
967
|
+
if (overwrite) {
|
|
968
|
+
await deps.writeFile(envPath, envContent);
|
|
969
|
+
wrote = true;
|
|
970
|
+
} else {
|
|
971
|
+
io.log("");
|
|
972
|
+
io.log(`Skipped writing ${envPath}. Credential block:`);
|
|
973
|
+
io.log("");
|
|
974
|
+
io.log(envContent);
|
|
975
|
+
}
|
|
976
|
+
} else {
|
|
977
|
+
await deps.writeFile(envPath, envContent);
|
|
978
|
+
wrote = true;
|
|
979
|
+
}
|
|
980
|
+
if (wrote) {
|
|
981
|
+
io.log("");
|
|
982
|
+
io.log(`Wrote ${envPath}`);
|
|
983
|
+
io.log("");
|
|
984
|
+
io.log("Next steps:");
|
|
985
|
+
io.log(" - Add .env.submit to .gitignore if it isn't already (it contains secrets).");
|
|
986
|
+
io.log(" - Run `extension-deploy --dry-run` to sanity-check the full submit flow.");
|
|
987
|
+
io.log("");
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
stores,
|
|
991
|
+
envPath,
|
|
992
|
+
envContent,
|
|
993
|
+
verified,
|
|
994
|
+
wrote
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
async function collectChromeCredentials(deps) {
|
|
998
|
+
const { io } = deps;
|
|
999
|
+
io.log("Chrome Web Store needs OAuth credentials. You'll need:");
|
|
1000
|
+
io.log(" 1. A Google Cloud project with the Chrome Web Store API enabled:");
|
|
1001
|
+
io.log(" https://console.cloud.google.com/apis/library/chromewebstore.googleapis.com");
|
|
1002
|
+
io.log(" 2. OAuth 2.0 credentials (application type: Desktop app):");
|
|
1003
|
+
io.log(" https://console.cloud.google.com/apis/credentials");
|
|
1004
|
+
io.log(" 3. Your extension ID from the Chrome Web Store Developer Dashboard.");
|
|
1005
|
+
io.log("");
|
|
1006
|
+
io.log("This wizard will take your Client ID + Secret and walk you through");
|
|
1007
|
+
io.log("generating a refresh token automatically.");
|
|
1008
|
+
io.log("");
|
|
1009
|
+
const extensionId = await requireValue(io, "Chrome extension ID", "(32-character ID from the CWS dashboard URL)");
|
|
1010
|
+
const clientId = await requireValue(io, "OAuth Client ID");
|
|
1011
|
+
const clientSecret = await requireValue(io, "OAuth Client Secret", void 0, {
|
|
1012
|
+
secret: true
|
|
1013
|
+
});
|
|
1014
|
+
const refreshToken = await obtainChromeRefreshToken(deps, clientId, clientSecret);
|
|
1015
|
+
return {
|
|
1016
|
+
extensionId,
|
|
1017
|
+
clientId,
|
|
1018
|
+
clientSecret,
|
|
1019
|
+
refreshToken
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function buildChromeAuthUrl(clientId) {
|
|
1023
|
+
const params = new URLSearchParams({
|
|
1024
|
+
response_type: "code",
|
|
1025
|
+
client_id: clientId,
|
|
1026
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1027
|
+
scope: CHROME_SCOPE,
|
|
1028
|
+
access_type: "offline",
|
|
1029
|
+
prompt: "consent"
|
|
1030
|
+
});
|
|
1031
|
+
return `${CHROME_OAUTH_AUTH_URL}?${params.toString()}`;
|
|
1032
|
+
}
|
|
1033
|
+
async function exchangeChromeAuthCode(input) {
|
|
1034
|
+
const fetchFn = input.fetchImpl ?? fetch;
|
|
1035
|
+
const body = new URLSearchParams({
|
|
1036
|
+
code: input.authCode,
|
|
1037
|
+
client_id: input.clientId,
|
|
1038
|
+
client_secret: input.clientSecret,
|
|
1039
|
+
redirect_uri: CHROME_OOB_REDIRECT,
|
|
1040
|
+
grant_type: "authorization_code"
|
|
1041
|
+
});
|
|
1042
|
+
const res = await fetchFn(CHROME_OAUTH_TOKEN_URL, {
|
|
1043
|
+
method: "POST",
|
|
1044
|
+
headers: {
|
|
1045
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
1046
|
+
},
|
|
1047
|
+
body: body.toString()
|
|
1048
|
+
});
|
|
1049
|
+
if (!res.ok) {
|
|
1050
|
+
const text = await res.text().catch(()=>"");
|
|
1051
|
+
throw new Error(`Chrome token exchange failed (HTTP ${res.status}): ${text || res.statusText}`);
|
|
1052
|
+
}
|
|
1053
|
+
const json = await res.json();
|
|
1054
|
+
if (!json.refresh_token) throw new Error(`Chrome token exchange returned no refresh_token${json.error ? `: ${json.error} \u{2014} ${json.error_description ?? ""}` : ""}. This usually means the authorization code was already used or expired; try the init wizard again.`);
|
|
1055
|
+
return json.refresh_token;
|
|
1056
|
+
}
|
|
1057
|
+
async function obtainChromeRefreshToken(deps, clientId, clientSecret) {
|
|
1058
|
+
const { io } = deps;
|
|
1059
|
+
const authUrl = buildChromeAuthUrl(clientId);
|
|
1060
|
+
io.log("");
|
|
1061
|
+
io.log("Open the URL below in your browser, approve the scope, and paste the");
|
|
1062
|
+
io.log("authorization code shown on the page:");
|
|
1063
|
+
io.log("");
|
|
1064
|
+
io.log(` ${authUrl}`);
|
|
1065
|
+
io.log("");
|
|
1066
|
+
const authCode = await requireValue(io, "Authorization code", void 0, {
|
|
1067
|
+
secret: true
|
|
1068
|
+
});
|
|
1069
|
+
return exchangeChromeAuthCode({
|
|
1070
|
+
clientId,
|
|
1071
|
+
clientSecret,
|
|
1072
|
+
authCode,
|
|
1073
|
+
fetchImpl: deps.fetchImpl
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
async function collectFirefoxCredentials(deps) {
|
|
1077
|
+
const { io } = deps;
|
|
1078
|
+
io.log("Firefox AMO needs JWT API credentials:");
|
|
1079
|
+
io.log(" 1. Sign in at https://addons.mozilla.org");
|
|
1080
|
+
io.log(" 2. Create API credentials at https://addons.mozilla.org/developers/addon/api/key/");
|
|
1081
|
+
io.log(" 3. Copy the JWT issuer and JWT secret (the secret is only shown once).");
|
|
1082
|
+
io.log("");
|
|
1083
|
+
const addonId = await requireValue(io, "Firefox addon ID", "({uuid} GUID or email-style ID from your addon page)");
|
|
1084
|
+
const jwtIssuer = await requireValue(io, "JWT issuer");
|
|
1085
|
+
const jwtSecret = await requireValue(io, "JWT secret", void 0, {
|
|
1086
|
+
secret: true
|
|
1087
|
+
});
|
|
1088
|
+
return {
|
|
1089
|
+
addonId,
|
|
1090
|
+
jwtIssuer,
|
|
1091
|
+
jwtSecret
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
async function collectEdgeCredentials(deps) {
|
|
1095
|
+
const { io } = deps;
|
|
1096
|
+
io.log("Edge Partner Center needs the v1.1 API credentials:");
|
|
1097
|
+
io.log(" 1. Open the Partner Center dashboard: https://partner.microsoft.com/dashboard/microsoftedge/overview");
|
|
1098
|
+
io.log(" 2. Under your extension, go to Settings \u2192 API access \u2192 API v1.1 and generate credentials.");
|
|
1099
|
+
io.log(" 3. Copy the Client ID, API key, and Product ID.");
|
|
1100
|
+
io.log("");
|
|
1101
|
+
const productId = await requireValue(io, "Edge product ID");
|
|
1102
|
+
const clientId = await requireValue(io, "Edge client ID");
|
|
1103
|
+
const apiKey = await requireValue(io, "Edge API key", void 0, {
|
|
1104
|
+
secret: true
|
|
1105
|
+
});
|
|
1106
|
+
return {
|
|
1107
|
+
productId,
|
|
1108
|
+
clientId,
|
|
1109
|
+
apiKey
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
function labelFor(store) {
|
|
1113
|
+
var _STORE_CHOICES_find;
|
|
1114
|
+
return (null == (_STORE_CHOICES_find = STORE_CHOICES.find((c)=>c.value === store)) ? void 0 : _STORE_CHOICES_find.label) ?? String(store);
|
|
1115
|
+
}
|
|
1116
|
+
function normalizeVerify(result) {
|
|
1117
|
+
return {
|
|
1118
|
+
ok: result.ok,
|
|
1119
|
+
message: result.message
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
function logVerify(io, store, record) {
|
|
1123
|
+
const marker = record.ok ? "[ok]" : "[fail]";
|
|
1124
|
+
io.log(` ${marker} verify ${store}: ${record.message}`);
|
|
1125
|
+
}
|
|
1126
|
+
function resolveEnvPath(cwd, outputPath) {
|
|
1127
|
+
const target = outputPath ?? ".env.submit";
|
|
1128
|
+
return external_node_path_default.isAbsolute(target) ? target : external_node_path_default.join(cwd, target);
|
|
1129
|
+
}
|
|
1130
|
+
async function requireValue(io, label, hint, opts) {
|
|
1131
|
+
const question = hint ? `${label} ${hint}:` : `${label}:`;
|
|
1132
|
+
for(let attempt = 0; attempt < 3; attempt++){
|
|
1133
|
+
const answer = (await io.prompt(question, opts)).trim();
|
|
1134
|
+
if (answer) return answer;
|
|
1135
|
+
io.log(` ${label} is required.`);
|
|
1136
|
+
}
|
|
1137
|
+
throw new Error(`init: ${label} is required.`);
|
|
1138
|
+
}
|
|
1139
|
+
function createStdinIO() {
|
|
1140
|
+
const rl = external_node_readline_default.createInterface({
|
|
1141
|
+
input: process.stdin,
|
|
1142
|
+
output: process.stdout,
|
|
1143
|
+
terminal: false
|
|
1144
|
+
});
|
|
1145
|
+
const lineIter = rl[Symbol.asyncIterator]();
|
|
1146
|
+
async function readLine() {
|
|
1147
|
+
const { value, done } = await lineIter.next();
|
|
1148
|
+
if (done) return "";
|
|
1149
|
+
return value;
|
|
1150
|
+
}
|
|
1151
|
+
function write(chunk) {
|
|
1152
|
+
process.stdout.write(chunk);
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
async prompt (question, opts) {
|
|
1156
|
+
const suffix = (null == opts ? void 0 : opts.default) ? ` [${opts.default}]` : "";
|
|
1157
|
+
const secretNote = (null == opts ? void 0 : opts.secret) ? " (input visible)" : "";
|
|
1158
|
+
write(`${question}${suffix}${secretNote} `);
|
|
1159
|
+
const answer = await readLine();
|
|
1160
|
+
if (0 === answer.length && (null == opts ? void 0 : opts.default)) return opts.default;
|
|
1161
|
+
return answer;
|
|
1162
|
+
},
|
|
1163
|
+
async confirm (question, defaultValue = false) {
|
|
1164
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
1165
|
+
write(`${question} ${suffix} `);
|
|
1166
|
+
const raw = (await readLine()).trim().toLowerCase();
|
|
1167
|
+
if ("" === raw) return defaultValue;
|
|
1168
|
+
return "y" === raw || "yes" === raw;
|
|
1169
|
+
},
|
|
1170
|
+
async multiselect (question, choices) {
|
|
1171
|
+
write(`${question}\n`);
|
|
1172
|
+
choices.forEach((choice, idx)=>{
|
|
1173
|
+
write(` ${idx + 1}) ${choice.label}\n`);
|
|
1174
|
+
});
|
|
1175
|
+
write(' Enter comma-separated numbers (e.g. 1,2 or "all"): ');
|
|
1176
|
+
const raw = (await readLine()).trim().toLowerCase();
|
|
1177
|
+
if ("all" === raw) return choices.map((c)=>c.value);
|
|
1178
|
+
const indices = raw.split(/[,\s]+/).map((token)=>Number.parseInt(token, 10)).filter((n)=>Number.isInteger(n) && n >= 1 && n <= choices.length);
|
|
1179
|
+
const unique = Array.from(new Set(indices));
|
|
1180
|
+
return unique.map((i)=>choices[i - 1].value);
|
|
1181
|
+
},
|
|
1182
|
+
log (message) {
|
|
1183
|
+
write(`${message}\n`);
|
|
1184
|
+
},
|
|
1185
|
+
dispose () {
|
|
1186
|
+
rl.close();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
const stdinFs = {
|
|
1191
|
+
async writeFile (filePath, content) {
|
|
1192
|
+
await promises_default.writeFile(filePath, content, "utf-8");
|
|
1193
|
+
},
|
|
1194
|
+
async fileExists (filePath) {
|
|
1195
|
+
try {
|
|
1196
|
+
await promises_default.access(filePath);
|
|
1197
|
+
return true;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return false;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
888
1203
|
function usage() {
|
|
889
1204
|
return `
|
|
890
1205
|
extension-deploy \u{2014} Deploy browser extensions to Chrome, Firefox, and Edge.
|
|
891
1206
|
|
|
892
1207
|
USAGE
|
|
893
1208
|
extension-deploy [options] submit to one or more stores
|
|
1209
|
+
extension-deploy init [options] interactive credential setup wizard
|
|
894
1210
|
extension-deploy watch --chrome [options] poll an in-flight submission
|
|
895
1211
|
extension-deploy watch --firefox [options]
|
|
896
1212
|
extension-deploy watch --edge [options]
|
|
@@ -931,6 +1247,12 @@ WATCH
|
|
|
931
1247
|
--watch-interval <seconds> Poll interval (default 60)
|
|
932
1248
|
--watch-timeout <seconds> Max time to wait for a terminal state (default 3600)
|
|
933
1249
|
|
|
1250
|
+
INIT
|
|
1251
|
+
--init-stores <list> Comma-separated store list (skip picker)
|
|
1252
|
+
--init-output <path> Destination file (default .env.submit)
|
|
1253
|
+
--init-skip-verify Don't run verifyCredentials after collection
|
|
1254
|
+
--init-force Overwrite existing .env.submit without asking
|
|
1255
|
+
|
|
934
1256
|
ENVIRONMENT VARIABLES
|
|
935
1257
|
All flags can be set via SCREAMING_SNAKE_CASE env vars (e.g. CHROME_ZIP,
|
|
936
1258
|
FIREFOX_JWT_SECRET, EDGE_API_KEY). Flags take precedence over env vars.
|
|
@@ -942,7 +1264,9 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
942
1264
|
"dryRun",
|
|
943
1265
|
"chromeReviewExemption",
|
|
944
1266
|
"chromeSkipSubmitReview",
|
|
945
|
-
"edgeSkipSubmitReview"
|
|
1267
|
+
"edgeSkipSubmitReview",
|
|
1268
|
+
"initSkipVerify",
|
|
1269
|
+
"initForce"
|
|
946
1270
|
]);
|
|
947
1271
|
const INT_FLAGS = new Set([
|
|
948
1272
|
"chromeDeployPercentage",
|
|
@@ -955,6 +1279,9 @@ function parseArgs(argv) {
|
|
|
955
1279
|
if ("watch" === argv[0]) {
|
|
956
1280
|
flags.command = "watch";
|
|
957
1281
|
i = 1;
|
|
1282
|
+
} else if ("init" === argv[0]) {
|
|
1283
|
+
flags.command = "init";
|
|
1284
|
+
i = 1;
|
|
958
1285
|
}
|
|
959
1286
|
while(i < argv.length){
|
|
960
1287
|
const arg = argv[i];
|
|
@@ -1029,6 +1356,7 @@ async function main() {
|
|
|
1029
1356
|
process.exit(0);
|
|
1030
1357
|
}
|
|
1031
1358
|
if ("watch" === flags.command) return void await runWatch(flags);
|
|
1359
|
+
if ("init" === flags.command) return void await runInitCommand(flags);
|
|
1032
1360
|
const config = resolveConfig(flags);
|
|
1033
1361
|
try {
|
|
1034
1362
|
const result = await deploy(config);
|
|
@@ -1097,4 +1425,37 @@ function mustValue(flagName, value) {
|
|
|
1097
1425
|
if (!value) throw new Error(`Missing required flag: ${flagName}`);
|
|
1098
1426
|
return value;
|
|
1099
1427
|
}
|
|
1428
|
+
async function runInitCommand(flags) {
|
|
1429
|
+
const stores = parseInitStores(flags.initStores);
|
|
1430
|
+
const io = createStdinIO();
|
|
1431
|
+
try {
|
|
1432
|
+
await runInit({
|
|
1433
|
+
io,
|
|
1434
|
+
cwd: process.cwd(),
|
|
1435
|
+
writeFile: stdinFs.writeFile,
|
|
1436
|
+
fileExists: stdinFs.fileExists
|
|
1437
|
+
}, {
|
|
1438
|
+
stores,
|
|
1439
|
+
outputPath: flags.initOutput,
|
|
1440
|
+
skipVerify: flags.initSkipVerify,
|
|
1441
|
+
force: flags.initForce
|
|
1442
|
+
});
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
} finally{
|
|
1447
|
+
io.dispose();
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function parseInitStores(raw) {
|
|
1451
|
+
if (!raw) return;
|
|
1452
|
+
const allowed = [
|
|
1453
|
+
"chrome",
|
|
1454
|
+
"firefox",
|
|
1455
|
+
"edge"
|
|
1456
|
+
];
|
|
1457
|
+
const parsed = raw.split(",").map((s)=>s.trim().toLowerCase()).filter((s)=>allowed.includes(s));
|
|
1458
|
+
if (0 === parsed.length) throw new Error(`--init-stores: expected a comma-separated list of chrome,firefox,edge (got "${raw}")`);
|
|
1459
|
+
return parsed;
|
|
1460
|
+
}
|
|
1100
1461
|
export { chromeOptionsSchema, deploy, deployConfigSchema, edgeOptionsSchema, firefoxOptionsSchema, getChromeSubmissionEvent, getEdgeSubmissionEvent, getFirefoxSubmissionEvent, main, resolveConfig, validateConfig, verifyChromeCredentials, verifyEdgeCredentials, verifyFirefoxCredentials, watchChromeSubmission, watchEdgeSubmission, watchFirefoxSubmission };
|
package/dist/src/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAsFzC,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiE3D;AAiCD,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAiC1C"}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -34,8 +34,8 @@ export interface CliFlags {
|
|
|
34
34
|
dryRun: boolean;
|
|
35
35
|
/** Path to write the final DeployResult as JSON (for CI integration). */
|
|
36
36
|
outputJson: string;
|
|
37
|
-
/** Subcommand: "deploy" (default) or "
|
|
38
|
-
command: "deploy" | "watch";
|
|
37
|
+
/** Subcommand: "deploy" (default), "watch", or "init". */
|
|
38
|
+
command: "deploy" | "watch" | "init";
|
|
39
39
|
/** Store targeted by the watch subcommand. */
|
|
40
40
|
watchStore: "chrome" | "firefox" | "edge";
|
|
41
41
|
/** Opaque submission ID (firefox version id, edge publish operation id). */
|
|
@@ -64,5 +64,13 @@ export interface CliFlags {
|
|
|
64
64
|
edgeClientId: string;
|
|
65
65
|
edgeApiKey: string;
|
|
66
66
|
edgeSkipSubmitReview: boolean;
|
|
67
|
+
/** Comma-separated store list for non-interactive init (e.g. "chrome,firefox"). */
|
|
68
|
+
initStores: string;
|
|
69
|
+
/** Target file path for init (default ".env.submit"). */
|
|
70
|
+
initOutput: string;
|
|
71
|
+
/** Skip running verifyCredentials after collecting values. */
|
|
72
|
+
initSkipVerify: boolean;
|
|
73
|
+
/** Overwrite an existing .env.submit without prompting. */
|
|
74
|
+
initForce: boolean;
|
|
67
75
|
}
|
|
68
76
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/src/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAEhE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,GACvB,YAAY,CAiDd;AA2CD,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB/C;AAiBD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AAEhE,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,CAAC,QAAQ,CAAC,GACvB,YAAY,CAiDd;AA2CD,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB/C;AAiBD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IACrC,8CAA8C;IAC9C,UAAU,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IAC1C,4EAA4E;IAC5E,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gDAAgD;IAChD,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sBAAsB,EAAE,OAAO,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,cAAc,EAAE,OAAO,CAAC;IACxB,2DAA2D;IAC3D,SAAS,EAAE,OAAO,CAAC;CACpB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { InitIO } from "./init";
|
|
2
|
+
/**
|
|
3
|
+
* Production stdin-based IO for the init wizard.
|
|
4
|
+
*
|
|
5
|
+
* We consume stdin via a readline async iterator instead of rl.question().
|
|
6
|
+
* Under Node 23, question() against a non-TTY stdin hangs on the second
|
|
7
|
+
* sequential call — the line iterator works consistently for both TTY and
|
|
8
|
+
* piped stdin, which matters for CI / test / shell-pipeline use.
|
|
9
|
+
*
|
|
10
|
+
* We intentionally don't mask echo for secret prompts. The _writeToOutput
|
|
11
|
+
* trick is fragile and the downside is small: users on their own terminal.
|
|
12
|
+
* A note is printed so users aren't surprised.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createStdinIO(): InitIO & {
|
|
15
|
+
dispose: () => void;
|
|
16
|
+
};
|
|
17
|
+
export declare const stdinFs: {
|
|
18
|
+
writeFile(filePath: string, content: string): Promise<void>;
|
|
19
|
+
fileExists(filePath: string): Promise<boolean>;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=init-io.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-io.d.ts","sourceRoot":"","sources":["../../src/init-io.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAEnD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CA4DhE;AAED,eAAO,MAAM,OAAO;wBACQ,MAAM,WAAW,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;yBAGtC,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAQrD,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { verifyChromeCredentials } from "./chrome";
|
|
2
|
+
import { verifyFirefoxCredentials } from "./firefox";
|
|
3
|
+
import { verifyEdgeCredentials } from "./edge";
|
|
4
|
+
export type InitStoreKey = "chrome" | "firefox" | "edge";
|
|
5
|
+
export interface InitIO {
|
|
6
|
+
prompt(question: string, opts?: {
|
|
7
|
+
secret?: boolean;
|
|
8
|
+
default?: string;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
confirm(question: string, defaultValue?: boolean): Promise<boolean>;
|
|
11
|
+
multiselect(question: string, choices: readonly {
|
|
12
|
+
value: InitStoreKey;
|
|
13
|
+
label: string;
|
|
14
|
+
}[]): Promise<InitStoreKey[]>;
|
|
15
|
+
log(message: string): void;
|
|
16
|
+
}
|
|
17
|
+
export interface InitDeps {
|
|
18
|
+
io: InitIO;
|
|
19
|
+
cwd: string;
|
|
20
|
+
writeFile: (filePath: string, content: string) => Promise<void>;
|
|
21
|
+
fileExists: (filePath: string) => Promise<boolean>;
|
|
22
|
+
fetchImpl?: typeof fetch;
|
|
23
|
+
verifyChrome?: typeof verifyChromeCredentials;
|
|
24
|
+
verifyFirefox?: typeof verifyFirefoxCredentials;
|
|
25
|
+
verifyEdge?: typeof verifyEdgeCredentials;
|
|
26
|
+
}
|
|
27
|
+
export interface InitOptions {
|
|
28
|
+
/** Skip the interactive store picker. */
|
|
29
|
+
stores?: InitStoreKey[];
|
|
30
|
+
/** Skip running verifyCredentials for each store after collecting creds. */
|
|
31
|
+
skipVerify?: boolean;
|
|
32
|
+
/** Overwrite .env.submit without prompting. */
|
|
33
|
+
force?: boolean;
|
|
34
|
+
/** Destination file (default ".env.submit"). Relative to cwd if not absolute. */
|
|
35
|
+
outputPath?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface StoreVerifyRecord {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
message: string;
|
|
40
|
+
}
|
|
41
|
+
export interface InitResult {
|
|
42
|
+
stores: InitStoreKey[];
|
|
43
|
+
envPath: string;
|
|
44
|
+
envContent: string;
|
|
45
|
+
verified: Partial<Record<InitStoreKey, StoreVerifyRecord>>;
|
|
46
|
+
wrote: boolean;
|
|
47
|
+
}
|
|
48
|
+
export declare function runInit(deps: InitDeps, options?: InitOptions): Promise<InitResult>;
|
|
49
|
+
export interface ChromeCredentials {
|
|
50
|
+
extensionId: string;
|
|
51
|
+
clientId: string;
|
|
52
|
+
clientSecret: string;
|
|
53
|
+
refreshToken: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function collectChromeCredentials(deps: InitDeps): Promise<ChromeCredentials>;
|
|
56
|
+
export declare function buildChromeAuthUrl(clientId: string): string;
|
|
57
|
+
export declare function exchangeChromeAuthCode(input: {
|
|
58
|
+
clientId: string;
|
|
59
|
+
clientSecret: string;
|
|
60
|
+
authCode: string;
|
|
61
|
+
fetchImpl?: typeof fetch;
|
|
62
|
+
}): Promise<string>;
|
|
63
|
+
export interface FirefoxCredentials {
|
|
64
|
+
addonId: string;
|
|
65
|
+
jwtIssuer: string;
|
|
66
|
+
jwtSecret: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function collectFirefoxCredentials(deps: InitDeps): Promise<FirefoxCredentials>;
|
|
69
|
+
export interface EdgeCredentials {
|
|
70
|
+
productId: string;
|
|
71
|
+
clientId: string;
|
|
72
|
+
apiKey: string;
|
|
73
|
+
}
|
|
74
|
+
export declare function collectEdgeCredentials(deps: InitDeps): Promise<EdgeCredentials>;
|
|
75
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init.ts"],"names":[],"mappings":"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"}
|