hotcodepush 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/commands/app/create.js +49 -0
- package/dist/commands/app/delete.js +34 -0
- package/dist/commands/app/get.js +33 -0
- package/dist/commands/app/list.js +38 -0
- package/dist/commands/app/transfer.js +58 -0
- package/dist/commands/app/update.js +34 -0
- package/dist/commands/audience/get.js +69 -0
- package/dist/commands/audit-log/list.js +82 -0
- package/dist/commands/binary/create.js +191 -0
- package/dist/commands/binary/get.js +56 -0
- package/dist/commands/binary/list.js +58 -0
- package/dist/commands/bundle/delete.js +35 -0
- package/dist/commands/bundle/get.js +63 -0
- package/dist/commands/bundle/list.js +66 -0
- package/dist/commands/bundle/upload.js +177 -0
- package/dist/commands/channel/create.js +59 -0
- package/dist/commands/channel/delete.js +68 -0
- package/dist/commands/channel/get.js +45 -0
- package/dist/commands/channel/list.js +39 -0
- package/dist/commands/channel/pause.js +32 -0
- package/dist/commands/channel/resume.js +32 -0
- package/dist/commands/channel/update.js +40 -0
- package/dist/commands/checkout/create.js +25 -0
- package/dist/commands/completion.js +37 -0
- package/dist/commands/customer-portal-session/create.js +25 -0
- package/dist/commands/device/delete.js +29 -0
- package/dist/commands/device/get.js +60 -0
- package/dist/commands/device/list.js +97 -0
- package/dist/commands/device/probe.js +253 -0
- package/dist/commands/doctor.js +364 -0
- package/dist/commands/fingerprint/diff.js +134 -0
- package/dist/commands/fingerprint.js +42 -0
- package/dist/commands/init.js +493 -0
- package/dist/commands/invitation/accept.js +53 -0
- package/dist/commands/invitation/create.js +52 -0
- package/dist/commands/invitation/delete.js +60 -0
- package/dist/commands/invitation/list.js +40 -0
- package/dist/commands/limit/list.js +34 -0
- package/dist/commands/login.js +36 -0
- package/dist/commands/logout.js +81 -0
- package/dist/commands/member/delete.js +35 -0
- package/dist/commands/member/get.js +37 -0
- package/dist/commands/member/list.js +41 -0
- package/dist/commands/member/update.js +53 -0
- package/dist/commands/open.js +37 -0
- package/dist/commands/organization/create.js +30 -0
- package/dist/commands/organization/delete.js +39 -0
- package/dist/commands/organization/get.js +38 -0
- package/dist/commands/organization/list.js +41 -0
- package/dist/commands/organization/update.js +64 -0
- package/dist/commands/release/create.js +237 -0
- package/dist/commands/release/get.js +81 -0
- package/dist/commands/release/list.js +69 -0
- package/dist/commands/release/pause.js +36 -0
- package/dist/commands/release/resume.js +36 -0
- package/dist/commands/release/revoke.js +254 -0
- package/dist/commands/release/rollback.js +73 -0
- package/dist/commands/release/rollout.js +63 -0
- package/dist/commands/release/update.js +82 -0
- package/dist/commands/resource-file/write.js +78 -0
- package/dist/commands/signing-key/add.js +45 -0
- package/dist/commands/signing-key/create.js +84 -0
- package/dist/commands/signing-key/delete.js +67 -0
- package/dist/commands/signing-key/list.js +38 -0
- package/dist/commands/sso-provider/delete.js +32 -0
- package/dist/commands/sso-provider/get.js +63 -0
- package/dist/commands/sso-provider/set.js +161 -0
- package/dist/commands/sso-provider/verify.js +31 -0
- package/dist/commands/statistics/get.js +189 -0
- package/dist/commands/usage/get.js +31 -0
- package/dist/commands/usage/list.js +41 -0
- package/dist/commands/webhook/create.js +72 -0
- package/dist/commands/webhook/delete.js +32 -0
- package/dist/commands/webhook/get.js +34 -0
- package/dist/commands/webhook/list.js +40 -0
- package/dist/commands/webhook/test.js +32 -0
- package/dist/commands/webhook/update.js +56 -0
- package/dist/commands/webhook-delivery/list.js +63 -0
- package/dist/commands/webhook-delivery/redeliver.js +48 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/completion/hotcodepush.bash +536 -0
- package/dist/completion/hotcodepush.fish +979 -0
- package/dist/completion/hotcodepush.zsh +1372 -0
- package/dist/config/consts.js +32 -0
- package/dist/index.js +5 -0
- package/dist/registry.js +83 -0
- package/dist/utils/api-client.js +17 -0
- package/dist/utils/audience.js +9 -0
- package/dist/utils/auth-client.js +35 -0
- package/dist/utils/authentication.js +60 -0
- package/dist/utils/boolean-flag.js +3 -0
- package/dist/utils/browser.js +6 -0
- package/dist/utils/build-step.js +104 -0
- package/dist/utils/bundle-files.js +80 -0
- package/dist/utils/bundle-resolution.js +46 -0
- package/dist/utils/channel-fields.js +130 -0
- package/dist/utils/cli.js +80 -0
- package/dist/utils/command-registry.js +79 -0
- package/dist/utils/command.js +4 -0
- package/dist/utils/completion/bash.js +48 -0
- package/dist/utils/completion/command-tree.js +72 -0
- package/dist/utils/completion/fish.js +44 -0
- package/dist/utils/completion/posix-case-arms.js +9 -0
- package/dist/utils/completion/posix-quote.js +4 -0
- package/dist/utils/completion/script-files.js +26 -0
- package/dist/utils/completion/zsh.js +72 -0
- package/dist/utils/compressed-files.js +31 -0
- package/dist/utils/credential.js +15 -0
- package/dist/utils/device-resolution.js +17 -0
- package/dist/utils/diagnostics.js +20 -0
- package/dist/utils/duration.js +16 -0
- package/dist/utils/environment.js +22 -0
- package/dist/utils/error-mapping.js +75 -0
- package/dist/utils/errors.js +266 -0
- package/dist/utils/fingerprint.js +76 -0
- package/dist/utils/flag-name.js +4 -0
- package/dist/utils/fleet.js +9 -0
- package/dist/utils/framework.js +74 -0
- package/dist/utils/frameworks/binary-create-step.js +70 -0
- package/dist/utils/frameworks/capacitor.js +150 -0
- package/dist/utils/frameworks/cordova.js +110 -0
- package/dist/utils/frameworks/expo.js +152 -0
- package/dist/utils/frameworks/index.js +13 -0
- package/dist/utils/frameworks/native-glue.js +10 -0
- package/dist/utils/frameworks/native-projects.js +27 -0
- package/dist/utils/frameworks/react-native-build.js +182 -0
- package/dist/utils/frameworks/react-native.js +127 -0
- package/dist/utils/frameworks/sdk-package.js +63 -0
- package/dist/utils/git-provenance.js +71 -0
- package/dist/utils/global-options.js +34 -0
- package/dist/utils/hosts.js +53 -0
- package/dist/utils/init-steps.js +51 -0
- package/dist/utils/json-file.js +28 -0
- package/dist/utils/login.js +153 -0
- package/dist/utils/member-resolution.js +49 -0
- package/dist/utils/native-project-edit.js +41 -0
- package/dist/utils/nullable-field.js +10 -0
- package/dist/utils/outcome.js +24 -0
- package/dist/utils/output.js +38 -0
- package/dist/utils/pack.js +20 -0
- package/dist/utils/package-json.js +5 -0
- package/dist/utils/package-manager.js +50 -0
- package/dist/utils/pagination.js +29 -0
- package/dist/utils/progress.js +23 -0
- package/dist/utils/progression.js +119 -0
- package/dist/utils/project-config.js +61 -0
- package/dist/utils/prompts.js +53 -0
- package/dist/utils/react-native-project.js +119 -0
- package/dist/utils/release-conditions.js +79 -0
- package/dist/utils/release-output.js +13 -0
- package/dist/utils/release-resolution.js +83 -0
- package/dist/utils/resource-file.js +81 -0
- package/dist/utils/resource-resolution.js +107 -0
- package/dist/utils/signing-private-key.js +105 -0
- package/dist/utils/spending-cap.js +16 -0
- package/dist/utils/telemetry.js +172 -0
- package/dist/utils/token-store.js +63 -0
- package/dist/utils/update-notice.js +68 -0
- package/dist/utils/upload.js +148 -0
- package/dist/utils/usage-month.js +14 -0
- package/dist/utils/user-config.js +54 -0
- package/dist/utils/webhook-resolution.js +47 -0
- package/dist/utils/xcode-project.js +181 -0
- package/package.json +11 -9
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { defineConfig, processConfig } from 'zodline';
|
|
3
|
+
import { ensureAuthentication } from './authentication.js';
|
|
4
|
+
import { loadCommands, resolveInvocation } from './command-registry.js';
|
|
5
|
+
import { isColorEnabled } from './environment.js';
|
|
6
|
+
import { printError, resolveCliError } from './error-mapping.js';
|
|
7
|
+
import { ExitCode, ReportedFailureError } from './errors.js';
|
|
8
|
+
import { printTelemetryNotice, reportFinishedCommand } from './telemetry.js';
|
|
9
|
+
import { printUpdateNotice, refreshLatestKnownVersion, } from './update-notice.js';
|
|
10
|
+
export async function runCli(commandRegistry, argv, meta) {
|
|
11
|
+
if (isShellStartup(argv)) {
|
|
12
|
+
const { cliError } = await runCommandLine(commandRegistry, argv, meta);
|
|
13
|
+
printCommandLineError(cliError, argv);
|
|
14
|
+
return cliError?.exitCode ?? ExitCode.Success;
|
|
15
|
+
}
|
|
16
|
+
const startedAt = performance.now();
|
|
17
|
+
printTelemetryNotice();
|
|
18
|
+
// The check runs beside the command, so the notice after it never waits long on the registry.
|
|
19
|
+
const latestKnownVersionRefresh = refreshLatestKnownVersion();
|
|
20
|
+
const { cliError, commandName } = await runCommandLine(commandRegistry, argv, meta);
|
|
21
|
+
printCommandLineError(cliError, argv);
|
|
22
|
+
if (commandName !== undefined) {
|
|
23
|
+
await reportFinishedCommand({
|
|
24
|
+
cliError,
|
|
25
|
+
commandName,
|
|
26
|
+
durationMs: performance.now() - startedAt,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
await latestKnownVersionRefresh;
|
|
30
|
+
printUpdateNotice();
|
|
31
|
+
return cliError?.exitCode ?? ExitCode.Success;
|
|
32
|
+
}
|
|
33
|
+
// A shell profile runs `completion` at startup, so it must not wait on the network or print a notice.
|
|
34
|
+
function isShellStartup(argv) {
|
|
35
|
+
return argv[0] === 'completion';
|
|
36
|
+
}
|
|
37
|
+
function printCommandLineError(cliError, argv) {
|
|
38
|
+
if (cliError !== undefined && !(cliError instanceof ReportedFailureError)) {
|
|
39
|
+
printError(cliError, {
|
|
40
|
+
isJson: argv.includes('--json'),
|
|
41
|
+
isVerbose: argv.includes('--verbose'),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function runCommandLine(commandRegistry, argv, meta) {
|
|
46
|
+
let commandName;
|
|
47
|
+
try {
|
|
48
|
+
const invocation = resolveInvocation(Object.keys(commandRegistry), argv);
|
|
49
|
+
// A line without a command shows the help or the version, and zodline ends the process there.
|
|
50
|
+
commandName =
|
|
51
|
+
invocation.commandNames.length === 1
|
|
52
|
+
? invocation.commandNames[0]
|
|
53
|
+
: undefined;
|
|
54
|
+
const commands = await loadCommands(commandRegistry, invocation.commandNames);
|
|
55
|
+
const config = defineConfig({ commands, meta });
|
|
56
|
+
const { args, command, options } = isColorEnabled(process.stdout)
|
|
57
|
+
? processConfig(config, invocation.argv)
|
|
58
|
+
: processConfigWithoutColor(config, invocation.argv);
|
|
59
|
+
if (commandName !== undefined &&
|
|
60
|
+
commands[commandName]?.authentication === 'required') {
|
|
61
|
+
await ensureAuthentication(options);
|
|
62
|
+
}
|
|
63
|
+
await command.action(options, args);
|
|
64
|
+
return { cliError: undefined, commandName };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return { cliError: resolveCliError(error), commandName };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// zodline always colours its help, so the colour codes are stripped while it runs.
|
|
71
|
+
function processConfigWithoutColor(config, argv) {
|
|
72
|
+
const log = console.log;
|
|
73
|
+
console.log = (...values) => log(...values.map(value => typeof value === 'string' ? stripVTControlCharacters(value) : value));
|
|
74
|
+
try {
|
|
75
|
+
return processConfig(config, argv);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
console.log = log;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { UnknownCommandError } from './errors.js';
|
|
2
|
+
const MAX_SUGGESTION_DISTANCE = 2;
|
|
3
|
+
const SHORT_FLAG_EXPANSIONS = {
|
|
4
|
+
'-h': '--help',
|
|
5
|
+
'-v': '--version',
|
|
6
|
+
};
|
|
7
|
+
export async function loadCommands(commandRegistry, commandNames) {
|
|
8
|
+
const invokedEntries = Object.entries(commandRegistry).filter(([name]) => commandNames.includes(name));
|
|
9
|
+
const loadedEntries = await Promise.all(invokedEntries.map(async ([name, importCommand]) => [
|
|
10
|
+
name,
|
|
11
|
+
(await importCommand()).default,
|
|
12
|
+
]));
|
|
13
|
+
return Object.fromEntries(loadedEntries);
|
|
14
|
+
}
|
|
15
|
+
export function resolveClosestCommandName(commandNames, words) {
|
|
16
|
+
let closestCommandName;
|
|
17
|
+
let closestDistance = MAX_SUGGESTION_DISTANCE + 1;
|
|
18
|
+
for (const commandName of commandNames) {
|
|
19
|
+
const typedCommand = words
|
|
20
|
+
.slice(0, commandName.split(' ').length)
|
|
21
|
+
.join(' ');
|
|
22
|
+
const distance = resolveEditDistance(typedCommand, commandName);
|
|
23
|
+
if (distance < closestDistance) {
|
|
24
|
+
closestCommandName = commandName;
|
|
25
|
+
closestDistance = distance;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return closestCommandName;
|
|
29
|
+
}
|
|
30
|
+
export function resolveCommandName(commandNames, words) {
|
|
31
|
+
for (let wordCount = words.length; wordCount > 0; wordCount--) {
|
|
32
|
+
const candidate = words.slice(0, wordCount).join(' ');
|
|
33
|
+
if (commandNames.includes(candidate)) {
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
// A line without a command, or with a noun alone, shows the help instead of failing.
|
|
40
|
+
export function resolveInvocation(commandNames, argv) {
|
|
41
|
+
const expandedArgv = argv.map(arg => SHORT_FLAG_EXPANSIONS[arg] ?? arg);
|
|
42
|
+
if (expandedArgv.includes('--version')) {
|
|
43
|
+
return { argv: ['--version'], commandNames: [] };
|
|
44
|
+
}
|
|
45
|
+
const words = resolveCommandWords(expandedArgv);
|
|
46
|
+
if (words.length === 0) {
|
|
47
|
+
return { argv: withHelpFlag(expandedArgv), commandNames };
|
|
48
|
+
}
|
|
49
|
+
const commandName = resolveCommandName(commandNames, words);
|
|
50
|
+
if (commandName) {
|
|
51
|
+
return { argv: expandedArgv, commandNames: [commandName] };
|
|
52
|
+
}
|
|
53
|
+
const nounCommandNames = commandNames.filter(name => name.startsWith(`${words[0]} `));
|
|
54
|
+
if (words.length === 1 && nounCommandNames.length > 0) {
|
|
55
|
+
return { argv: withHelpFlag(expandedArgv), commandNames: nounCommandNames };
|
|
56
|
+
}
|
|
57
|
+
throw new UnknownCommandError(words.join(' '), resolveClosestCommandName(commandNames, words));
|
|
58
|
+
}
|
|
59
|
+
function resolveCommandWords(argv) {
|
|
60
|
+
const firstFlagIndex = argv.findIndex(arg => arg.startsWith('-'));
|
|
61
|
+
return firstFlagIndex === -1 ? argv : argv.slice(0, firstFlagIndex);
|
|
62
|
+
}
|
|
63
|
+
// Levenshtein distance.
|
|
64
|
+
function resolveEditDistance(source, target) {
|
|
65
|
+
let previousRow = Array.from({ length: target.length + 1 }, (_, index) => index);
|
|
66
|
+
for (let sourceIndex = 1; sourceIndex <= source.length; sourceIndex++) {
|
|
67
|
+
const currentRow = [sourceIndex];
|
|
68
|
+
for (let targetIndex = 1; targetIndex <= target.length; targetIndex++) {
|
|
69
|
+
const substitutionCost = source[sourceIndex - 1] === target[targetIndex - 1] ? 0 : 1;
|
|
70
|
+
currentRow.push(Math.min((previousRow[targetIndex] ?? 0) + 1, (currentRow[targetIndex - 1] ?? 0) + 1, (previousRow[targetIndex - 1] ?? 0) + substitutionCost));
|
|
71
|
+
}
|
|
72
|
+
previousRow = currentRow;
|
|
73
|
+
}
|
|
74
|
+
return previousRow[target.length] ?? 0;
|
|
75
|
+
}
|
|
76
|
+
// zodline reads a repeated flag as an array, so `--help` is added only when it is absent.
|
|
77
|
+
function withHelpFlag(argv) {
|
|
78
|
+
return argv.includes('--help') ? argv : [...argv, '--help'];
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { buildPosixCaseArms } from './posix-case-arms.js';
|
|
2
|
+
import { quotePosixWord } from './posix-quote.js';
|
|
3
|
+
export function buildBashScript(commands) {
|
|
4
|
+
return [
|
|
5
|
+
'_hotcodepush() {',
|
|
6
|
+
' local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}',
|
|
7
|
+
' if (( COMP_CWORD == 1 )); then',
|
|
8
|
+
` ${buildReply(commands.map(({ name }) => name))}`,
|
|
9
|
+
' return',
|
|
10
|
+
' fi',
|
|
11
|
+
' if (( COMP_CWORD == 2 )) && [[ $cur != -* ]]; then',
|
|
12
|
+
' case ${COMP_WORDS[1]} in',
|
|
13
|
+
...commands
|
|
14
|
+
.filter(({ verbs }) => verbs.length > 0)
|
|
15
|
+
.map(({ name, verbs }) => ` ${quotePosixWord(name)}) ${buildReply(verbs.map(verb => verb.name))}; return ;;`),
|
|
16
|
+
' esac',
|
|
17
|
+
' fi',
|
|
18
|
+
' case "${COMP_WORDS[1]} ${COMP_WORDS[2]}" in',
|
|
19
|
+
...buildPosixCaseArms(commands, buildOptionsArm),
|
|
20
|
+
' esac',
|
|
21
|
+
'}',
|
|
22
|
+
'',
|
|
23
|
+
'complete -o default -F _hotcodepush hotcodepush',
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
// A command that takes arguments offers its flags only for a word starting with `-`.
|
|
27
|
+
// Its arguments fall back to the file names.
|
|
28
|
+
function buildOptionsArm(pattern, { hasArguments, options }) {
|
|
29
|
+
const enumOptions = options.filter(({ choices }) => choices.length > 0);
|
|
30
|
+
const freeValueFlags = options
|
|
31
|
+
.filter(({ choices, hasValue }) => hasValue && choices.length === 0)
|
|
32
|
+
.map(({ name }) => quotePosixWord(`--${name}`));
|
|
33
|
+
const flagReply = buildReply(options.map(({ name }) => `--${name}`));
|
|
34
|
+
return [
|
|
35
|
+
` ${pattern})`,
|
|
36
|
+
' case $prev in',
|
|
37
|
+
...enumOptions.map(({ choices, name }) => ` ${quotePosixWord(`--${name}`)}) ${buildReply(choices)} ;;`),
|
|
38
|
+
...(freeValueFlags.length > 0
|
|
39
|
+
? [` ${freeValueFlags.join('|')}) ;;`]
|
|
40
|
+
: []),
|
|
41
|
+
` *) ${hasArguments ? `[[ $cur == -* ]] && ${flagReply}` : flagReply} ;;`,
|
|
42
|
+
' esac',
|
|
43
|
+
' ;;',
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
function buildReply(words) {
|
|
47
|
+
return `COMPREPLY=($(compgen -W ${quotePosixWord(words.join(' '))} -- "$cur"))`;
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { resolveFlagName } from '../flag-name.js';
|
|
3
|
+
// A noun that runs nothing itself is described by its verbs.
|
|
4
|
+
export function resolveCommandTree(commands) {
|
|
5
|
+
const commandEntries = Object.entries(commands);
|
|
6
|
+
const topLevelNames = new Set(commandEntries.map(([commandName]) => commandName.split(' ')[0] ?? ''));
|
|
7
|
+
return [...topLevelNames].map(topLevelName => {
|
|
8
|
+
const verbs = commandEntries
|
|
9
|
+
.filter(([commandName]) => commandName.startsWith(`${topLevelName} `))
|
|
10
|
+
.map(([commandName, command]) => resolveCompletionCommand(commandName.slice(topLevelName.length + 1), command));
|
|
11
|
+
const topLevelCommand = commands[topLevelName];
|
|
12
|
+
return topLevelCommand === undefined
|
|
13
|
+
? {
|
|
14
|
+
description: verbs.map(({ name }) => name).join(', '),
|
|
15
|
+
hasArguments: false,
|
|
16
|
+
name: topLevelName,
|
|
17
|
+
options: [],
|
|
18
|
+
verbs,
|
|
19
|
+
}
|
|
20
|
+
: { ...resolveCompletionCommand(topLevelName, topLevelCommand), verbs };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// A union counts too, since `--protected` also takes `true` or `false` as text.
|
|
24
|
+
function isBooleanFlag(schema) {
|
|
25
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) {
|
|
26
|
+
return isBooleanFlag(schema.unwrap());
|
|
27
|
+
}
|
|
28
|
+
if (schema instanceof z.ZodUnion) {
|
|
29
|
+
return schema.options.some(isBooleanFlag);
|
|
30
|
+
}
|
|
31
|
+
return schema instanceof z.ZodBoolean;
|
|
32
|
+
}
|
|
33
|
+
// zodline fills an array from a repeated flag. A comma list is text piped into an array and given once.
|
|
34
|
+
function isRepeatableFlag(schema) {
|
|
35
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) {
|
|
36
|
+
return isRepeatableFlag(schema.unwrap());
|
|
37
|
+
}
|
|
38
|
+
return schema instanceof z.ZodArray;
|
|
39
|
+
}
|
|
40
|
+
// The pipe of a comma list splits the text before it checks each choice.
|
|
41
|
+
function resolveChoices(schema) {
|
|
42
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) {
|
|
43
|
+
return resolveChoices(schema.unwrap());
|
|
44
|
+
}
|
|
45
|
+
if (schema instanceof z.ZodPipe) {
|
|
46
|
+
return resolveChoices(schema.out);
|
|
47
|
+
}
|
|
48
|
+
if (schema instanceof z.ZodArray) {
|
|
49
|
+
return resolveChoices(schema.element);
|
|
50
|
+
}
|
|
51
|
+
return schema instanceof z.ZodEnum ? schema.options.map(String) : [];
|
|
52
|
+
}
|
|
53
|
+
function resolveCompletionCommand(name, command) {
|
|
54
|
+
const shape = command.options?.schema.shape ?? {};
|
|
55
|
+
return {
|
|
56
|
+
description: command.description ?? '',
|
|
57
|
+
hasArguments: command.args !== undefined,
|
|
58
|
+
name,
|
|
59
|
+
options: Object.entries(shape).map(([key, schema]) => resolveCompletionOption(key, schema)),
|
|
60
|
+
verbs: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function resolveCompletionOption(key, schema) {
|
|
64
|
+
const hasValue = !isBooleanFlag(schema);
|
|
65
|
+
return {
|
|
66
|
+
choices: hasValue ? resolveChoices(schema) : [],
|
|
67
|
+
description: schema.description ?? '',
|
|
68
|
+
hasValue,
|
|
69
|
+
isRepeatable: isRepeatableFlag(schema),
|
|
70
|
+
name: resolveFlagName(key),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function buildFishScript(commands) {
|
|
2
|
+
return [
|
|
3
|
+
'function __hotcodepush_needs_command',
|
|
4
|
+
' test (count (commandline -opc)) -eq 1',
|
|
5
|
+
'end',
|
|
6
|
+
'',
|
|
7
|
+
'function __hotcodepush_needs_verb --argument-names noun',
|
|
8
|
+
' set -l tokens (commandline -opc)',
|
|
9
|
+
' test (count $tokens) -eq 2; and test "$tokens[2]" = "$noun"',
|
|
10
|
+
'end',
|
|
11
|
+
'',
|
|
12
|
+
'function __hotcodepush_using --argument-names first_word second_word',
|
|
13
|
+
' set -l tokens (commandline -opc)',
|
|
14
|
+
' test "$tokens[2]" = "$first_word"; and test -z "$second_word" -o "$tokens[3]" = "$second_word"',
|
|
15
|
+
'end',
|
|
16
|
+
'',
|
|
17
|
+
...commands.map(({ description, name }) => `complete -c hotcodepush -f -n __hotcodepush_needs_command -a ${quoteFishWord(name)} -d ${quoteFishWord(description)}`),
|
|
18
|
+
...commands.flatMap(noun => noun.verbs.map(({ description, name }) => `complete -c hotcodepush -f -n ${quoteFishWord(`__hotcodepush_needs_verb ${noun.name}`)} -a ${quoteFishWord(name)} -d ${quoteFishWord(description)}`)),
|
|
19
|
+
...commands.flatMap(command => [
|
|
20
|
+
...buildOptionLines(resolveStandaloneCondition(command), command.options),
|
|
21
|
+
...command.verbs.flatMap(verb => buildOptionLines(`__hotcodepush_using ${command.name} ${verb.name}`, verb.options)),
|
|
22
|
+
]),
|
|
23
|
+
].join('\n');
|
|
24
|
+
}
|
|
25
|
+
function buildOptionLines(condition, options) {
|
|
26
|
+
return options.map(option => `complete -c hotcodepush -n ${quoteFishWord(condition)} -l ${option.name}${buildValueArguments(option)} -d ${quoteFishWord(option.description)}`);
|
|
27
|
+
}
|
|
28
|
+
function buildValueArguments({ choices, hasValue }) {
|
|
29
|
+
if (choices.length > 0) {
|
|
30
|
+
return ` -x -a ${quoteFishWord(choices.join(' '))}`;
|
|
31
|
+
}
|
|
32
|
+
return hasValue ? ' -r' : '';
|
|
33
|
+
}
|
|
34
|
+
// In fish, a backslash and a single quote are the only escapes inside single quotes.
|
|
35
|
+
function quoteFishWord(text) {
|
|
36
|
+
return `'${text.replace(/[\\']/g, '\\$&')}'`;
|
|
37
|
+
}
|
|
38
|
+
// Excludes the verbs, so `fingerprint` does not also match `fingerprint diff`.
|
|
39
|
+
function resolveStandaloneCondition({ name, verbs, }) {
|
|
40
|
+
return [
|
|
41
|
+
`__hotcodepush_using ${name}`,
|
|
42
|
+
...verbs.map(verb => `not __hotcodepush_using ${name} ${verb.name}`),
|
|
43
|
+
].join('; and ');
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { quotePosixWord } from './posix-quote.js';
|
|
2
|
+
// The verb arms come first, so `fingerprint diff` matches before `fingerprint`.
|
|
3
|
+
export function buildPosixCaseArms(commands, buildCaseArm) {
|
|
4
|
+
const verbArms = commands.flatMap(noun => noun.verbs.map(verb => buildCaseArm(quotePosixWord(`${noun.name} ${verb.name}`), verb, 2)));
|
|
5
|
+
const standaloneArms = commands
|
|
6
|
+
.filter(({ options }) => options.length > 0)
|
|
7
|
+
.map(command => buildCaseArm(`${quotePosixWord(`${command.name} `)}*`, command, 1));
|
|
8
|
+
return [...verbArms, ...standaloneArms].flat();
|
|
9
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { COMPLETION_DIRECTORY_PATH } from '../../config/consts.js';
|
|
4
|
+
import { loadCommands } from '../command-registry.js';
|
|
5
|
+
import { buildBashScript } from './bash.js';
|
|
6
|
+
import { resolveCommandTree } from './command-tree.js';
|
|
7
|
+
import { buildFishScript } from './fish.js';
|
|
8
|
+
import { buildZshScript } from './zsh.js';
|
|
9
|
+
export const COMPLETION_SHELLS = ['bash', 'fish', 'zsh'];
|
|
10
|
+
const SCRIPT_BUILDERS = {
|
|
11
|
+
bash: buildBashScript,
|
|
12
|
+
fish: buildFishScript,
|
|
13
|
+
zsh: buildZshScript,
|
|
14
|
+
};
|
|
15
|
+
export async function readCompletionScript(shell) {
|
|
16
|
+
return readFile(resolveCompletionScriptPath(shell), 'utf8');
|
|
17
|
+
}
|
|
18
|
+
// The build runs this once, so a shell start loads no command.
|
|
19
|
+
export async function writeCompletionScripts(commandRegistry) {
|
|
20
|
+
const commands = resolveCommandTree(await loadCommands(commandRegistry, Object.keys(commandRegistry)));
|
|
21
|
+
await mkdir(COMPLETION_DIRECTORY_PATH, { recursive: true });
|
|
22
|
+
await Promise.all(COMPLETION_SHELLS.map(shell => writeFile(resolveCompletionScriptPath(shell), SCRIPT_BUILDERS[shell](commands))));
|
|
23
|
+
}
|
|
24
|
+
function resolveCompletionScriptPath(shell) {
|
|
25
|
+
return join(COMPLETION_DIRECTORY_PATH, `hotcodepush.${shell}`);
|
|
26
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { buildPosixCaseArms } from './posix-case-arms.js';
|
|
2
|
+
import { quotePosixWord } from './posix-quote.js';
|
|
3
|
+
export function buildZshScript(commands) {
|
|
4
|
+
return [
|
|
5
|
+
'#compdef hotcodepush',
|
|
6
|
+
'',
|
|
7
|
+
'_hotcodepush() {',
|
|
8
|
+
' local -a completions',
|
|
9
|
+
' if (( CURRENT == 2 )); then',
|
|
10
|
+
...buildDescribeLines(' ', 'hotcodepush command', commands),
|
|
11
|
+
' fi',
|
|
12
|
+
' if (( CURRENT == 3 )) && [[ ${words[CURRENT]} != -* ]]; then',
|
|
13
|
+
' case ${words[2]} in',
|
|
14
|
+
...commands
|
|
15
|
+
.filter(({ verbs }) => verbs.length > 0)
|
|
16
|
+
.flatMap(({ name, verbs }) => [
|
|
17
|
+
` ${quotePosixWord(name)})`,
|
|
18
|
+
...buildDescribeLines(' ', `${name} command`, verbs),
|
|
19
|
+
' ;;',
|
|
20
|
+
]),
|
|
21
|
+
' esac',
|
|
22
|
+
' fi',
|
|
23
|
+
' case "${words[2]} ${words[3]}" in',
|
|
24
|
+
...buildPosixCaseArms(commands, (pattern, command, commandWordCount) => buildArgumentsArm(pattern, commandWordCount, command)),
|
|
25
|
+
' esac',
|
|
26
|
+
'}',
|
|
27
|
+
'',
|
|
28
|
+
'# drops the command words, so _arguments reads the flags as the words of the command they belong to',
|
|
29
|
+
'_hotcodepush_arguments() {',
|
|
30
|
+
' local command_word_count=$1',
|
|
31
|
+
' shift',
|
|
32
|
+
' shift $command_word_count words',
|
|
33
|
+
' (( CURRENT -= command_word_count ))',
|
|
34
|
+
' _arguments "$@"',
|
|
35
|
+
'}',
|
|
36
|
+
'',
|
|
37
|
+
'compdef _hotcodepush hotcodepush',
|
|
38
|
+
'',
|
|
39
|
+
'# under fpath this file is the body of _hotcodepush: its first run defines the function, then completes with it',
|
|
40
|
+
'if [ "$funcstack[1]" = "_hotcodepush" ]; then',
|
|
41
|
+
' _hotcodepush "$@"',
|
|
42
|
+
'fi',
|
|
43
|
+
].join('\n');
|
|
44
|
+
}
|
|
45
|
+
function buildArgumentsArm(pattern, commandWordCount, { hasArguments, options }) {
|
|
46
|
+
const specs = [
|
|
47
|
+
...options.map(buildOptionSpec),
|
|
48
|
+
...(hasArguments ? ['*:argument:_files'] : []),
|
|
49
|
+
];
|
|
50
|
+
return [
|
|
51
|
+
` ${pattern})`,
|
|
52
|
+
` _hotcodepush_arguments ${commandWordCount} \\`,
|
|
53
|
+
...specs.map(spec => ` ${quotePosixWord(spec)} \\`),
|
|
54
|
+
' ;;',
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function buildDescribeLines(indentation, tag, commands) {
|
|
58
|
+
return [
|
|
59
|
+
`${indentation}completions=(`,
|
|
60
|
+
...commands.map(({ description, name }) => `${indentation} ${quotePosixWord(`${name}:${description}`)}`),
|
|
61
|
+
`${indentation})`,
|
|
62
|
+
`${indentation}_describe -t commands ${quotePosixWord(tag)} completions`,
|
|
63
|
+
`${indentation}return`,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function buildOptionSpec({ choices, description, hasValue, isRepeatable, name, }) {
|
|
67
|
+
const flag = `${isRepeatable ? '*' : ''}--${name}[${description.replace(/[[\]\\]/g, '\\$&')}]`;
|
|
68
|
+
if (!hasValue) {
|
|
69
|
+
return flag;
|
|
70
|
+
}
|
|
71
|
+
return `${flag}:${name}:${choices.length > 0 ? `(${choices.join(' ')})` : '_files'}`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
2
|
+
import { mkdtemp, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { createGzip } from 'node:zlib';
|
|
7
|
+
export async function compressFiles(files, temporaryDirectoryPath) {
|
|
8
|
+
const compressedFiles = new Map();
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
if (compressedFiles.has(file.sha256)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const compressedFilePath = join(temporaryDirectoryPath, `${file.sha256}.gz`);
|
|
14
|
+
await pipeline(createReadStream(file.filePath), createGzip(), createWriteStream(compressedFilePath));
|
|
15
|
+
compressedFiles.set(file.sha256, {
|
|
16
|
+
compressedFilePath,
|
|
17
|
+
sha256: file.sha256,
|
|
18
|
+
sizeBytes: (await stat(compressedFilePath)).size,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return compressedFiles;
|
|
22
|
+
}
|
|
23
|
+
export async function withTemporaryDirectory(task) {
|
|
24
|
+
const temporaryDirectoryPath = await mkdtemp(join(tmpdir(), 'hotcodepush-'));
|
|
25
|
+
try {
|
|
26
|
+
return await task(temporaryDirectoryPath);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await rm(temporaryDirectoryPath, { force: true, recursive: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HotCodePushError } from '@hotcodepush/node';
|
|
2
|
+
import { createApiClient } from './api-client.js';
|
|
3
|
+
const UNAUTHENTICATED_STATUS = 401;
|
|
4
|
+
export async function fetchCurrentUser() {
|
|
5
|
+
return createApiClient().users.get({ userId: 'me' });
|
|
6
|
+
}
|
|
7
|
+
export function isUnauthenticatedError(error) {
|
|
8
|
+
return (error instanceof HotCodePushError && error.status === UNAUTHENTICATED_STATUS);
|
|
9
|
+
}
|
|
10
|
+
export function resolveCredentialText(user, isEnvironmentToken) {
|
|
11
|
+
const who = `${user.name} (${user.email})`;
|
|
12
|
+
return isEnvironmentToken
|
|
13
|
+
? `Authenticated with HOTCODEPUSH_TOKEN as ${who}.`
|
|
14
|
+
: `Logged in as ${who}.`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { readProjectConfig } from './project-config.js';
|
|
3
|
+
import { promptText } from './prompts.js';
|
|
4
|
+
import { fetchAppId } from './resource-resolution.js';
|
|
5
|
+
export const deviceOptionShape = {
|
|
6
|
+
device: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe('The device, by the id it generated, as the debug screen and device list show it.'),
|
|
10
|
+
};
|
|
11
|
+
// A device has no name to pick from, so a missing one is asked for as text.
|
|
12
|
+
export async function resolveDeviceReference(hotCodePush, options) {
|
|
13
|
+
const appId = await fetchAppId(hotCodePush, options, readProjectConfig(options.config));
|
|
14
|
+
const deviceId = options.device ??
|
|
15
|
+
(await promptText('--device', 'Which device id?', options));
|
|
16
|
+
return { appId, deviceId };
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { log } from '@clack/prompts';
|
|
3
|
+
import { isColorEnabled } from './environment.js';
|
|
4
|
+
// A diagnostic addresses the person, so it goes to stderr in the gutter the prompts use; stdout stays the command's result.
|
|
5
|
+
const STDERR = { output: process.stderr };
|
|
6
|
+
export function printError({ docsUrl, sentences }) {
|
|
7
|
+
const styledDocsUrl = isColorEnabled(process.stderr)
|
|
8
|
+
? styleText('dim', docsUrl, { validateStream: false })
|
|
9
|
+
: docsUrl;
|
|
10
|
+
log.error([...sentences, styledDocsUrl].join('\n'), STDERR);
|
|
11
|
+
}
|
|
12
|
+
export function printNotice(text) {
|
|
13
|
+
log.info(text, STDERR);
|
|
14
|
+
}
|
|
15
|
+
export function printSuccess(text) {
|
|
16
|
+
log.success(text, STDERR);
|
|
17
|
+
}
|
|
18
|
+
export function printWarning(text) {
|
|
19
|
+
log.warn(text, STDERR);
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const DURATION_PATTERN = /^\d+[dh]$/;
|
|
2
|
+
const MILLISECONDS_PER_DURATION_UNIT = {
|
|
3
|
+
d: 24 * 60 * 60 * 1000,
|
|
4
|
+
h: 60 * 60 * 1000,
|
|
5
|
+
};
|
|
6
|
+
// The caller has checked the duration against `DURATION_PATTERN`.
|
|
7
|
+
export function resolveDurationMilliseconds(duration) {
|
|
8
|
+
const unit = duration.slice(-1);
|
|
9
|
+
return Number(duration.slice(0, -1)) * MILLISECONDS_PER_DURATION_UNIT[unit];
|
|
10
|
+
}
|
|
11
|
+
// A duration such as 2h means that long ago. Anything else is passed on as a timestamp.
|
|
12
|
+
export function resolveTimeBound(bound) {
|
|
13
|
+
return bound !== undefined && DURATION_PATTERN.test(bound)
|
|
14
|
+
? new Date(Date.now() - resolveDurationMilliseconds(bound)).toISOString()
|
|
15
|
+
: bound;
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Follows the colour rules of clig.dev.
|
|
2
|
+
export function isColorEnabled(stream) {
|
|
3
|
+
return (Boolean(stream.isTTY) &&
|
|
4
|
+
!process.env.NO_COLOR &&
|
|
5
|
+
process.env.TERM !== 'dumb');
|
|
6
|
+
}
|
|
7
|
+
// GitHub Actions and the other CI runners set `CI`.
|
|
8
|
+
export function isCi() {
|
|
9
|
+
return Boolean(process.env.CI);
|
|
10
|
+
}
|
|
11
|
+
// For a build that is never shipped.
|
|
12
|
+
export function isOfflineBuild() {
|
|
13
|
+
return process.env.HOTCODEPUSH_OFFLINE === '1';
|
|
14
|
+
}
|
|
15
|
+
// A prompt in a pipeline would hang the job, even where a TTY exists.
|
|
16
|
+
export function isInteractive({ json, yes }) {
|
|
17
|
+
return (Boolean(process.stdin.isTTY) &&
|
|
18
|
+
Boolean(process.stdout.isTTY) &&
|
|
19
|
+
!isCi() &&
|
|
20
|
+
!json &&
|
|
21
|
+
!yes);
|
|
22
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { inspect, stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { HotCodePushError } from '@hotcodepush/node';
|
|
3
|
+
import { ZodError } from 'zod';
|
|
4
|
+
import { ZodlineError } from 'zodline';
|
|
5
|
+
import { ERRORS_DOCS_URL } from '../config/consts.js';
|
|
6
|
+
import { printError as printErrorDiagnostic } from './diagnostics.js';
|
|
7
|
+
import { ApiError, CliError, InvalidParameterError, UnexpectedError, UnexpectedResponseError, } from './errors.js';
|
|
8
|
+
import { resolveFlagName } from './flag-name.js';
|
|
9
|
+
import { printJson } from './output.js';
|
|
10
|
+
// Matches the end of a sentence. A message from zodline or an unexpected error may lack it.
|
|
11
|
+
const SENTENCE_END_PATTERN = /[.!?]$/;
|
|
12
|
+
const UNEXPECTED_RESPONSE_CODE = 'unexpected_response';
|
|
13
|
+
export function buildErrorJson({ code, fix, message }) {
|
|
14
|
+
return { error: { code, message, fix } };
|
|
15
|
+
}
|
|
16
|
+
export function resolveSentence(message) {
|
|
17
|
+
return SENTENCE_END_PATTERN.test(message) ? message : `${message}.`;
|
|
18
|
+
}
|
|
19
|
+
export function resolveErrorDocsUrl({ code }) {
|
|
20
|
+
return `${ERRORS_DOCS_URL}#${code}`;
|
|
21
|
+
}
|
|
22
|
+
// An API error message says what to do itself, so it has no fix.
|
|
23
|
+
export function resolveErrorSentences({ fix, message }) {
|
|
24
|
+
return fix === null ? [message] : [resolveSentence(message), fix];
|
|
25
|
+
}
|
|
26
|
+
export function printError(cliError, { isJson, isVerbose }) {
|
|
27
|
+
if (isVerbose) {
|
|
28
|
+
process.stderr.write(`${inspect(cliError)}\n`);
|
|
29
|
+
}
|
|
30
|
+
if (isJson) {
|
|
31
|
+
printJson(buildErrorJson(cliError));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
printErrorDiagnostic({
|
|
35
|
+
docsUrl: resolveErrorDocsUrl(cliError),
|
|
36
|
+
sentences: resolveErrorSentences(cliError),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// A response without a code, such as a proxy HTML page, is not an error from the API catalog.
|
|
41
|
+
export function resolveApiError({ code, error, error_description, message, status, statusText, }) {
|
|
42
|
+
const receivedCode = code ?? error;
|
|
43
|
+
if (receivedCode === undefined) {
|
|
44
|
+
return new UnexpectedResponseError(status);
|
|
45
|
+
}
|
|
46
|
+
return new ApiError(receivedCode, message ?? error_description ?? statusText, status);
|
|
47
|
+
}
|
|
48
|
+
export function resolveCliError(error) {
|
|
49
|
+
if (error instanceof CliError) {
|
|
50
|
+
return error;
|
|
51
|
+
}
|
|
52
|
+
if (error instanceof HotCodePushError) {
|
|
53
|
+
return error.code === UNEXPECTED_RESPONSE_CODE
|
|
54
|
+
? new UnexpectedResponseError(error.status, error)
|
|
55
|
+
: new ApiError(error.code, error.message, error.status);
|
|
56
|
+
}
|
|
57
|
+
if (error instanceof ZodError) {
|
|
58
|
+
return new InvalidParameterError(resolveZodErrorMessage(error), undefined, error);
|
|
59
|
+
}
|
|
60
|
+
if (error instanceof ZodlineError) {
|
|
61
|
+
return new InvalidParameterError(resolveSentence(stripVTControlCharacters(error.message)), undefined, error);
|
|
62
|
+
}
|
|
63
|
+
return new UnexpectedError(error);
|
|
64
|
+
}
|
|
65
|
+
function resolveZodErrorMessage(error) {
|
|
66
|
+
const [issue] = error.issues;
|
|
67
|
+
if (!issue) {
|
|
68
|
+
return error.message;
|
|
69
|
+
}
|
|
70
|
+
const [key] = issue.path;
|
|
71
|
+
if (typeof key !== 'string') {
|
|
72
|
+
return issue.message;
|
|
73
|
+
}
|
|
74
|
+
return `The value of --${resolveFlagName(key)} is invalid. ${issue.message}.`;
|
|
75
|
+
}
|