hotcodepush 0.0.1 → 0.1.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 +189 -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 +176 -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 +36 -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 +363 -0
- package/dist/commands/fingerprint/diff.js +133 -0
- package/dist/commands/fingerprint.js +41 -0
- package/dist/commands/init.js +492 -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 +185 -0
- package/dist/commands/logout.js +79 -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 +253 -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 +76 -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/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 +75 -0
- package/dist/utils/command-registry.js +79 -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/duration.js +16 -0
- package/dist/utils/environment.js +22 -0
- package/dist/utils/error-mapping.js +79 -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/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 +37 -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 +80 -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 +171 -0
- package/dist/utils/token-store.js +63 -0
- package/dist/utils/update-notice.js +67 -0
- package/dist/utils/upload.js +148 -0
- package/dist/utils/usage-month.js +14 -0
- package/dist/utils/user-config.js +46 -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,32 @@
|
|
|
1
|
+
import { defineCommand } from 'zodline';
|
|
2
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
3
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
4
|
+
import { printJson } from '../../utils/output.js';
|
|
5
|
+
import { confirmConsequence } from '../../utils/prompts.js';
|
|
6
|
+
import { channelOptionShape, fetchChannel, } from '../../utils/resource-resolution.js';
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
description: 'Pause a channel, the kill switch: it serves nothing new until resumed.',
|
|
9
|
+
examples: [
|
|
10
|
+
'hotcodepush channel pause --channel production',
|
|
11
|
+
'hotcodepush channel pause --channel production --yes --json',
|
|
12
|
+
],
|
|
13
|
+
options: defineCommandOptions(channelOptionShape),
|
|
14
|
+
action: async (options) => {
|
|
15
|
+
const hotCodePush = createApiClient();
|
|
16
|
+
const fetchedChannel = await fetchChannel(hotCodePush, options);
|
|
17
|
+
const isConfirmed = await confirmConsequence(`pauses channel ${fetchedChannel.name}: devices keep what they have and get nothing new`, options);
|
|
18
|
+
if (!isConfirmed) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const pausedChannel = await hotCodePush.apps.channels.pause({
|
|
22
|
+
appId: fetchedChannel.appId,
|
|
23
|
+
channelId: fetchedChannel.id,
|
|
24
|
+
});
|
|
25
|
+
if (options.json) {
|
|
26
|
+
printJson(pausedChannel);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(`Paused channel ${pausedChannel.name} (${pausedChannel.id}).`);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineCommand } from 'zodline';
|
|
2
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
3
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
4
|
+
import { printJson } from '../../utils/output.js';
|
|
5
|
+
import { confirmConsequence } from '../../utils/prompts.js';
|
|
6
|
+
import { channelOptionShape, fetchChannel, } from '../../utils/resource-resolution.js';
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
description: 'Resume a paused channel: it serves its releases again.',
|
|
9
|
+
examples: [
|
|
10
|
+
'hotcodepush channel resume --channel production',
|
|
11
|
+
'hotcodepush channel resume --channel production --yes --json',
|
|
12
|
+
],
|
|
13
|
+
options: defineCommandOptions(channelOptionShape),
|
|
14
|
+
action: async (options) => {
|
|
15
|
+
const hotCodePush = createApiClient();
|
|
16
|
+
const fetchedChannel = await fetchChannel(hotCodePush, options);
|
|
17
|
+
const isConfirmed = await confirmConsequence(`resumes channel ${fetchedChannel.name}: devices receive its releases again`, options);
|
|
18
|
+
if (!isConfirmed) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const resumedChannel = await hotCodePush.apps.channels.resume({
|
|
22
|
+
appId: fetchedChannel.appId,
|
|
23
|
+
channelId: fetchedChannel.id,
|
|
24
|
+
});
|
|
25
|
+
if (options.json) {
|
|
26
|
+
printJson(resumedChannel);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log(`Resumed channel ${resumedChannel.name} (${resumedChannel.id}).`);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
4
|
+
import { channelFieldsClearShape, channelFieldsShape, resolveChannelFields, } from '../../utils/channel-fields.js';
|
|
5
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
6
|
+
import { printJson } from '../../utils/output.js';
|
|
7
|
+
import { channelOptionShape, fetchChannel, } from '../../utils/resource-resolution.js';
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
description: 'Rename a channel or change its flags, expiry and auto-pause policy.',
|
|
10
|
+
examples: [
|
|
11
|
+
'hotcodepush channel update --channel staging --protected',
|
|
12
|
+
'hotcodepush channel update --channel pr-42 --expires-in 7d --json',
|
|
13
|
+
],
|
|
14
|
+
options: defineCommandOptions({
|
|
15
|
+
...channelFieldsClearShape,
|
|
16
|
+
...channelFieldsShape,
|
|
17
|
+
...channelOptionShape,
|
|
18
|
+
name: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe("The channel's new name: letters, digits, - and _, one to 64 characters."),
|
|
22
|
+
}),
|
|
23
|
+
action: async (options) => {
|
|
24
|
+
const hotCodePush = createApiClient();
|
|
25
|
+
const channelFields = resolveChannelFields(options);
|
|
26
|
+
const fetchedChannel = await fetchChannel(hotCodePush, options);
|
|
27
|
+
const updatedChannel = await hotCodePush.apps.channels.update({
|
|
28
|
+
...channelFields,
|
|
29
|
+
appId: fetchedChannel.appId,
|
|
30
|
+
channelId: fetchedChannel.id,
|
|
31
|
+
name: options.name,
|
|
32
|
+
});
|
|
33
|
+
if (options.json) {
|
|
34
|
+
printJson(updatedChannel);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log(`Updated channel ${updatedChannel.name} (${updatedChannel.id}).`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineCommand } from 'zodline';
|
|
2
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
3
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
4
|
+
import { printJson } from '../../utils/output.js';
|
|
5
|
+
import { fetchOrganizationId } from '../../utils/resource-resolution.js';
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
description: 'Enable billing on a free organization: create the checkout for pay-as-you-go and print its URL to open.',
|
|
8
|
+
examples: [
|
|
9
|
+
'hotcodepush checkout create',
|
|
10
|
+
'hotcodepush checkout create --organization Acme --json',
|
|
11
|
+
],
|
|
12
|
+
options: defineCommandOptions({}),
|
|
13
|
+
action: async (options) => {
|
|
14
|
+
const hotCodePush = createApiClient();
|
|
15
|
+
const createdCheckout = await hotCodePush.organizations.checkouts.create({
|
|
16
|
+
organizationId: await fetchOrganizationId(hotCodePush, options),
|
|
17
|
+
});
|
|
18
|
+
if (options.json) {
|
|
19
|
+
printJson(createdCheckout);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(`Open the checkout to enable billing: ${createdCheckout.url}`);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { COMPLETION_SHELLS, readCompletionScript, } from '../utils/completion/script-files.js';
|
|
4
|
+
import { InvalidParameterError } from '../utils/errors.js';
|
|
5
|
+
import { defineCommandOptions } from '../utils/global-options.js';
|
|
6
|
+
import { printJson } from '../utils/output.js';
|
|
7
|
+
import { promptSelect } from '../utils/prompts.js';
|
|
8
|
+
// zodline reports an invalid argument as a plain error that would read as `unexpected`.
|
|
9
|
+
// So the action checks the shell itself.
|
|
10
|
+
const ShellSchema = z.enum(COMPLETION_SHELLS);
|
|
11
|
+
export default defineCommand({
|
|
12
|
+
description: 'Print the completion script for <shell>, bash, fish or zsh: eval "$(hotcodepush completion <shell>)" loads it from ~/.bashrc, or from ~/.zshrc after compinit, and fish loads it saved as ~/.config/fish/completions/hotcodepush.fish.',
|
|
13
|
+
examples: [
|
|
14
|
+
'eval "$(hotcodepush completion zsh)"',
|
|
15
|
+
'hotcodepush completion fish > ~/.config/fish/completions/hotcodepush.fish',
|
|
16
|
+
],
|
|
17
|
+
args: z.array(z.string()),
|
|
18
|
+
options: defineCommandOptions({}),
|
|
19
|
+
action: async (options, args) => {
|
|
20
|
+
if (args.length > 1) {
|
|
21
|
+
throw new InvalidParameterError(`The completion command takes one shell, <shell>. It does not take ${args.slice(1).join(' ')}.`);
|
|
22
|
+
}
|
|
23
|
+
const shell = args[0] ??
|
|
24
|
+
(await promptSelect('<shell>', 'Which shell is the completion script for?', ShellSchema.options.map(value => ({ label: value, value })), options));
|
|
25
|
+
const parsedShell = ShellSchema.safeParse(shell);
|
|
26
|
+
if (!parsedShell.success) {
|
|
27
|
+
throw new InvalidParameterError(`The shell ${shell} has no completion script.`, 'Pass bash, fish or zsh as <shell>.');
|
|
28
|
+
}
|
|
29
|
+
const script = await readCompletionScript(parsedShell.data);
|
|
30
|
+
if (options.json) {
|
|
31
|
+
printJson({ script });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log(script);
|
|
35
|
+
},
|
|
36
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineCommand } from 'zodline';
|
|
2
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
3
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
4
|
+
import { printJson } from '../../utils/output.js';
|
|
5
|
+
import { fetchOrganizationId } from '../../utils/resource-resolution.js';
|
|
6
|
+
export default defineCommand({
|
|
7
|
+
description: "Create a session of the customer portal for an organization's invoices and payment method, and print its URL to open.",
|
|
8
|
+
examples: [
|
|
9
|
+
'hotcodepush customer-portal-session create',
|
|
10
|
+
'hotcodepush customer-portal-session create --organization Acme --json',
|
|
11
|
+
],
|
|
12
|
+
options: defineCommandOptions({}),
|
|
13
|
+
action: async (options) => {
|
|
14
|
+
const hotCodePush = createApiClient();
|
|
15
|
+
const createdSession = await hotCodePush.organizations.customerPortalSessions.create({
|
|
16
|
+
organizationId: await fetchOrganizationId(hotCodePush, options),
|
|
17
|
+
});
|
|
18
|
+
if (options.json) {
|
|
19
|
+
printJson(createdSession);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(`Open the customer portal for the invoices and the payment method: ${createdSession.url}`);
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { defineCommand } from 'zodline';
|
|
2
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
3
|
+
import { deviceOptionShape, resolveDeviceReference, } from '../../utils/device-resolution.js';
|
|
4
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
5
|
+
import { printJson } from '../../utils/output.js';
|
|
6
|
+
import { confirmConsequence } from '../../utils/prompts.js';
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
description: "Delete a device's record, an erasure request; the device reports again with its next check.",
|
|
9
|
+
examples: [
|
|
10
|
+
'hotcodepush device delete --device 6b1e9d37-2f5c-4a80-9c46-d8e3a1f7b259',
|
|
11
|
+
'hotcodepush device delete --device 6b1e9d37-2f5c-4a80-9c46-d8e3a1f7b259 --yes --json',
|
|
12
|
+
],
|
|
13
|
+
options: defineCommandOptions(deviceOptionShape),
|
|
14
|
+
action: async (options) => {
|
|
15
|
+
const hotCodePush = createApiClient();
|
|
16
|
+
const { appId, deviceId } = await resolveDeviceReference(hotCodePush, options);
|
|
17
|
+
const isConfirmed = await confirmConsequence(`deletes device ${deviceId}'s record for good, the erasure request. The device reports again with its next check`, options);
|
|
18
|
+
if (!isConfirmed) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await hotCodePush.apps.devices.delete({ appId, deviceId });
|
|
22
|
+
if (options.json) {
|
|
23
|
+
printJson({ id: deviceId, name: null });
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log(`Deleted device ${deviceId}.`);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineCommand } from 'zodline';
|
|
2
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
3
|
+
import { deviceOptionShape, resolveDeviceReference, } from '../../utils/device-resolution.js';
|
|
4
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
5
|
+
import { printDetails, printJson, printTable, resolveDate, } from '../../utils/output.js';
|
|
6
|
+
import { fetchAllPages } from '../../utils/pagination.js';
|
|
7
|
+
import { resolveBinaryText } from './list.js';
|
|
8
|
+
export default defineCommand({
|
|
9
|
+
description: 'Print what a device runs and reported last, with its outcome per release, newest first.',
|
|
10
|
+
examples: [
|
|
11
|
+
'hotcodepush device get --device 6b1e9d37-2f5c-4a80-9c46-d8e3a1f7b259',
|
|
12
|
+
'hotcodepush device get --device 6b1e9d37-2f5c-4a80-9c46-d8e3a1f7b259 --json',
|
|
13
|
+
],
|
|
14
|
+
options: defineCommandOptions(deviceOptionShape),
|
|
15
|
+
action: async (options) => {
|
|
16
|
+
const hotCodePush = createApiClient();
|
|
17
|
+
const deviceReference = await resolveDeviceReference(hotCodePush, options);
|
|
18
|
+
const fetchedDevice = await hotCodePush.apps.devices.get({
|
|
19
|
+
...deviceReference,
|
|
20
|
+
relations: ['channel'],
|
|
21
|
+
});
|
|
22
|
+
const fetchedEvents = await fetchAllPages(page => hotCodePush.apps.devices.events.list({ ...deviceReference, ...page }));
|
|
23
|
+
if (options.json) {
|
|
24
|
+
printJson({ ...fetchedDevice, events: fetchedEvents });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
printDetails([
|
|
28
|
+
['ID', fetchedDevice.deviceId],
|
|
29
|
+
['Platform', `${fetchedDevice.platform} ${fetchedDevice.osVersion}`],
|
|
30
|
+
['Binary', resolveBinaryText(fetchedDevice)],
|
|
31
|
+
['Fingerprint', fetchedDevice.fingerprint ?? 'none'],
|
|
32
|
+
[
|
|
33
|
+
'Channel',
|
|
34
|
+
`${fetchedDevice.channel?.name ?? fetchedDevice.channelId ?? 'none'} (${fetchedDevice.channelSource})`,
|
|
35
|
+
],
|
|
36
|
+
['Release', fetchedDevice.currentReleaseId ?? 'the embedded bundle'],
|
|
37
|
+
['SDK', fetchedDevice.sdkVersion],
|
|
38
|
+
[
|
|
39
|
+
'Attributes',
|
|
40
|
+
Object.entries(fetchedDevice.attributes)
|
|
41
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
42
|
+
.join(', ') || 'none',
|
|
43
|
+
],
|
|
44
|
+
['Country', fetchedDevice.country ?? 'unknown'],
|
|
45
|
+
['Last seen', fetchedDevice.lastSeenAt],
|
|
46
|
+
['First seen', fetchedDevice.createdAt],
|
|
47
|
+
]);
|
|
48
|
+
printTable({
|
|
49
|
+
emptyText: 'No outcomes yet: the device has not checked a release.',
|
|
50
|
+
headers: ['RELEASE', 'OUTCOME', 'REASON', 'DATE'],
|
|
51
|
+
nextOffset: null,
|
|
52
|
+
rows: fetchedEvents.map(({ createdAt, reason, releaseId, type }) => [
|
|
53
|
+
releaseId,
|
|
54
|
+
type,
|
|
55
|
+
reason ?? '',
|
|
56
|
+
resolveDate(createdAt),
|
|
57
|
+
]),
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineCommand } from 'zodline';
|
|
3
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
4
|
+
import { resolveTimeBound } from '../../utils/duration.js';
|
|
5
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
6
|
+
import { printJson, printTable, resolveDate } from '../../utils/output.js';
|
|
7
|
+
import { paginationShape, resolveNextOffset } from '../../utils/pagination.js';
|
|
8
|
+
import { readProjectConfig } from '../../utils/project-config.js';
|
|
9
|
+
import { fetchAppId, fetchChannels, fetchResourceId, } from '../../utils/resource-resolution.js';
|
|
10
|
+
export default defineCommand({
|
|
11
|
+
description: "List the app's devices, the newest report first, filtered by what they reported; support and QA, never a pipeline.",
|
|
12
|
+
examples: [
|
|
13
|
+
'hotcodepush device list --attribute userId=42',
|
|
14
|
+
'hotcodepush device list --platform ios --binary-version 2.4.1 --last-seen-since 2026-09-01T00:00:00Z --json',
|
|
15
|
+
],
|
|
16
|
+
options: defineCommandOptions({
|
|
17
|
+
...paginationShape,
|
|
18
|
+
attribute: z
|
|
19
|
+
.string()
|
|
20
|
+
.optional()
|
|
21
|
+
.describe('An attribute the app set, key=value, such as userId=42.'),
|
|
22
|
+
binaryBuild: z
|
|
23
|
+
.string()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('The build number the devices run.'),
|
|
26
|
+
binaryVersion: z
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('The app version the devices run.'),
|
|
30
|
+
channel: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('The channel the devices follow, by id or name.'),
|
|
34
|
+
fingerprint: z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('The native fingerprint the devices report.'),
|
|
38
|
+
lastSeenSince: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe('The earliest last report, inclusive: an ISO 8601 timestamp or a duration ago such as 2h or 7d.'),
|
|
42
|
+
lastSeenUntil: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe('The latest last report, inclusive: an ISO 8601 timestamp or a duration ago such as 2h or 7d.'),
|
|
46
|
+
platform: z
|
|
47
|
+
.enum(['android', 'ios'])
|
|
48
|
+
.optional()
|
|
49
|
+
.describe('The platform, ios or android.'),
|
|
50
|
+
sdkVersion: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('The SDK version the devices run.'),
|
|
54
|
+
}),
|
|
55
|
+
action: async (options) => {
|
|
56
|
+
const hotCodePush = createApiClient();
|
|
57
|
+
const appId = await fetchAppId(hotCodePush, options, readProjectConfig(options.config));
|
|
58
|
+
const listedDevices = await hotCodePush.apps.devices.list({
|
|
59
|
+
appId,
|
|
60
|
+
attribute: options.attribute,
|
|
61
|
+
binaryBuild: options.binaryBuild,
|
|
62
|
+
binaryVersion: options.binaryVersion,
|
|
63
|
+
channelId: options.channel === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: await fetchResourceId('channel', options.channel, () => fetchChannels(hotCodePush, appId)),
|
|
66
|
+
fingerprint: options.fingerprint,
|
|
67
|
+
lastSeenSince: resolveTimeBound(options.lastSeenSince),
|
|
68
|
+
lastSeenUntil: resolveTimeBound(options.lastSeenUntil),
|
|
69
|
+
limit: options.limit,
|
|
70
|
+
offset: options.offset,
|
|
71
|
+
platform: options.platform,
|
|
72
|
+
relations: ['channel'],
|
|
73
|
+
sdkVersion: options.sdkVersion,
|
|
74
|
+
});
|
|
75
|
+
const nextOffset = resolveNextOffset(listedDevices.length, options);
|
|
76
|
+
if (options.json) {
|
|
77
|
+
printJson({ devices: listedDevices, nextOffset });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
printTable({
|
|
81
|
+
emptyText: 'No devices match; a device appears with its first check.',
|
|
82
|
+
headers: ['ID', 'PLATFORM', 'BINARY', 'CHANNEL', 'SDK', 'LAST SEEN'],
|
|
83
|
+
nextOffset,
|
|
84
|
+
rows: listedDevices.map(device => [
|
|
85
|
+
device.deviceId,
|
|
86
|
+
device.platform,
|
|
87
|
+
resolveBinaryText(device),
|
|
88
|
+
device.channel?.name ?? device.channelId ?? 'none',
|
|
89
|
+
device.sdkVersion,
|
|
90
|
+
resolveDate(device.lastSeenAt),
|
|
91
|
+
]),
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
export function resolveBinaryText({ binaryBuild, binaryVersion, }) {
|
|
96
|
+
return `${binaryVersion} (${binaryBuild})`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { ChannelIndexSchema, evaluateChannelIndex, } from '@hotcodepush/protocol';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { defineCommand } from 'zodline';
|
|
4
|
+
import { createApiClient } from '../../utils/api-client.js';
|
|
5
|
+
import { deviceOptionShape } from '../../utils/device-resolution.js';
|
|
6
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
7
|
+
import { resolveFilesBaseUrl } from '../../utils/hosts.js';
|
|
8
|
+
import { printDetails, printJson, printTable } from '../../utils/output.js';
|
|
9
|
+
import { fetchAllPages } from '../../utils/pagination.js';
|
|
10
|
+
import { readProjectConfig } from '../../utils/project-config.js';
|
|
11
|
+
import { promptSelect, promptText } from '../../utils/prompts.js';
|
|
12
|
+
import { resolveAttributePairs } from '../../utils/release-conditions.js';
|
|
13
|
+
import { channelOptionShape, fetchAppId, fetchChannelId, } from '../../utils/resource-resolution.js';
|
|
14
|
+
import { readApiUrl } from '../../utils/user-config.js';
|
|
15
|
+
const INDEX_FETCH_TIMEOUT_MS = 30_000;
|
|
16
|
+
// No release is older than the binary, so a device without a created binary has no floor.
|
|
17
|
+
const NO_FLOOR = new Date(0).toISOString();
|
|
18
|
+
export default defineCommand({
|
|
19
|
+
description: 'Answer why a device did not update: fetch the live index as a device would, evaluate it and mark every condition of every release.',
|
|
20
|
+
examples: [
|
|
21
|
+
'hotcodepush device probe --platform ios --binary-version 2.4.1 --binary-build 57',
|
|
22
|
+
'hotcodepush device probe --device 6b1e9d37-2f5c-4a80-9c46-d8e3a1f7b259 --json',
|
|
23
|
+
],
|
|
24
|
+
options: defineCommandOptions({
|
|
25
|
+
...channelOptionShape,
|
|
26
|
+
...deviceOptionShape,
|
|
27
|
+
attribute: z
|
|
28
|
+
.array(z.string())
|
|
29
|
+
.optional()
|
|
30
|
+
.describe('An attribute the app sets, key=value, repeatable.'),
|
|
31
|
+
binaryBuild: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("The store build's build number, CFBundleVersion or versionCode."),
|
|
35
|
+
binaryVersion: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("The store build's version, CFBundleShortVersionString or versionName."),
|
|
39
|
+
platform: z
|
|
40
|
+
.enum(['android', 'ios'])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('The platform, ios or android.'),
|
|
43
|
+
}),
|
|
44
|
+
action: async (options) => {
|
|
45
|
+
const hotCodePush = createApiClient();
|
|
46
|
+
const projectConfig = readProjectConfig(options.config);
|
|
47
|
+
const appId = await fetchAppId(hotCodePush, options, projectConfig);
|
|
48
|
+
const fetchedDevice = options.device === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: await hotCodePush.apps.devices.get({
|
|
51
|
+
appId,
|
|
52
|
+
deviceId: options.device,
|
|
53
|
+
});
|
|
54
|
+
const channel = await hotCodePush.apps.channels.get({
|
|
55
|
+
appId,
|
|
56
|
+
channelId: await fetchChannelId(hotCodePush, appId, {
|
|
57
|
+
...options,
|
|
58
|
+
channel: options.channel ?? fetchedDevice?.channelId ?? undefined,
|
|
59
|
+
}, projectConfig),
|
|
60
|
+
});
|
|
61
|
+
const platform = options.platform ??
|
|
62
|
+
fetchedDevice?.platform ??
|
|
63
|
+
(await promptSelect('--platform', 'Which platform?', [
|
|
64
|
+
{ label: 'iOS', value: 'ios' },
|
|
65
|
+
{ label: 'Android', value: 'android' },
|
|
66
|
+
], options));
|
|
67
|
+
const binaryVersion = options.binaryVersion ??
|
|
68
|
+
fetchedDevice?.binaryVersion ??
|
|
69
|
+
(await promptText('--binary-version', 'Which app version?', options));
|
|
70
|
+
const binaryBuild = options.binaryBuild ??
|
|
71
|
+
fetchedDevice?.binaryBuild ??
|
|
72
|
+
(await promptText('--binary-build', 'Which build number?', options));
|
|
73
|
+
const binary = (await fetchAllPages(page => hotCodePush.apps.binaries.list({ appId, ...page }))).find(candidate => candidate.platform === platform &&
|
|
74
|
+
candidate.version === binaryVersion &&
|
|
75
|
+
candidate.build === binaryBuild);
|
|
76
|
+
const servedIndex = await fetchServedIndex(appId, channel.id, platform);
|
|
77
|
+
const unknownFacts = resolveUnknownFacts(fetchedDevice, binary);
|
|
78
|
+
const builtAt = binary?.createdAt ?? null;
|
|
79
|
+
const device = {
|
|
80
|
+
attributes: {
|
|
81
|
+
...fetchedDevice?.attributes,
|
|
82
|
+
...Object.fromEntries(resolveAttributePairs(options.attribute).map(({ key, value }) => [
|
|
83
|
+
key,
|
|
84
|
+
value,
|
|
85
|
+
])),
|
|
86
|
+
},
|
|
87
|
+
binaryBuild,
|
|
88
|
+
binaryVersion,
|
|
89
|
+
builtAt: builtAt ?? NO_FLOOR,
|
|
90
|
+
currentRelease: fetchedDevice === undefined
|
|
91
|
+
? null
|
|
92
|
+
: await fetchCurrentRelease(hotCodePush, fetchedDevice, servedIndex),
|
|
93
|
+
deviceId: fetchedDevice?.deviceId ?? '',
|
|
94
|
+
failedBundleIds: fetchedDevice === undefined
|
|
95
|
+
? []
|
|
96
|
+
: resolveFailedBundleIds(await fetchAllPages(page => hotCodePush.apps.devices.events.list({
|
|
97
|
+
appId,
|
|
98
|
+
deviceId: fetchedDevice.deviceId,
|
|
99
|
+
...page,
|
|
100
|
+
})), servedIndex.index),
|
|
101
|
+
fingerprint: fetchedDevice?.fingerprint ?? binary?.fingerprint ?? null,
|
|
102
|
+
osVersion: fetchedDevice?.osVersion ?? '',
|
|
103
|
+
reportedAt: fetchedDevice?.lastSeenAt ?? null,
|
|
104
|
+
};
|
|
105
|
+
const evaluation = servedIndex.index === null
|
|
106
|
+
? null
|
|
107
|
+
: evaluateKnownFacts(servedIndex.index, device, unknownFacts);
|
|
108
|
+
if (options.json) {
|
|
109
|
+
printJson({
|
|
110
|
+
channelId: channel.id,
|
|
111
|
+
device: { ...device, builtAt },
|
|
112
|
+
outcome: evaluation?.outcome ?? null,
|
|
113
|
+
platform,
|
|
114
|
+
sequence: servedIndex.index?.sequence ?? null,
|
|
115
|
+
status: servedIndex.status,
|
|
116
|
+
unknownFacts,
|
|
117
|
+
url: servedIndex.url,
|
|
118
|
+
verdicts: evaluation?.verdicts ?? [],
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
printDetails([
|
|
123
|
+
['Channel', `${channel.name}, ${platform}`],
|
|
124
|
+
['Index', resolveServedIndexText(servedIndex)],
|
|
125
|
+
[
|
|
126
|
+
'Outcome',
|
|
127
|
+
evaluation === null ? 'none' : resolveOutcomeText(evaluation),
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
'Unknown',
|
|
131
|
+
unknownFacts.length === 0
|
|
132
|
+
? 'nothing'
|
|
133
|
+
: `${unknownFacts.join(', ')}, treated as passing; pass --device for the facts of a real device`,
|
|
134
|
+
],
|
|
135
|
+
]);
|
|
136
|
+
if (evaluation !== null && servedIndex.index !== null) {
|
|
137
|
+
printTable({
|
|
138
|
+
emptyText: 'The index offers no release.',
|
|
139
|
+
headers: ['RELEASE', 'ROLLOUT', 'CONDITIONS', 'VERDICT'],
|
|
140
|
+
nextOffset: null,
|
|
141
|
+
rows: evaluation.verdicts.map(verdict => [
|
|
142
|
+
`#${verdict.release.number}`,
|
|
143
|
+
`${verdict.release.rolloutPercent}%`,
|
|
144
|
+
resolveConditionsText(verdict, unknownFacts),
|
|
145
|
+
verdict.isEligible
|
|
146
|
+
? 'eligible'
|
|
147
|
+
: `${verdict.reason ?? 'skipped'}${verdict.conditionType === undefined ? '' : ` (${verdict.conditionType})`}`,
|
|
148
|
+
]),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
// The index may no longer carry the release, so its number then comes from the API.
|
|
154
|
+
async function fetchCurrentRelease(hotCodePush, fetchedDevice, servedIndex) {
|
|
155
|
+
const releaseId = fetchedDevice.currentReleaseId;
|
|
156
|
+
if (releaseId === null) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const indexRelease = servedIndex.index?.releases.find(({ id }) => id === releaseId);
|
|
160
|
+
const number = indexRelease?.number ??
|
|
161
|
+
(await hotCodePush.apps.releases.get({
|
|
162
|
+
appId: fetchedDevice.appId,
|
|
163
|
+
releaseId,
|
|
164
|
+
})).number;
|
|
165
|
+
return { id: releaseId, number };
|
|
166
|
+
}
|
|
167
|
+
// The same request a device makes.
|
|
168
|
+
async function fetchServedIndex(appId, channelId, platform) {
|
|
169
|
+
const url = `${resolveFilesBaseUrl(readApiUrl())}/apps/${appId}/channels/${channelId}/${platform}/v1/index.json`;
|
|
170
|
+
const response = await fetch(url, {
|
|
171
|
+
signal: AbortSignal.timeout(INDEX_FETCH_TIMEOUT_MS),
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
await response.body?.cancel();
|
|
175
|
+
return { index: null, status: response.status, url };
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
index: ChannelIndexSchema.parse(await response.json()),
|
|
179
|
+
status: response.status,
|
|
180
|
+
url,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// Rollouts count as reached and conditions on unknown facts as passing, so only known facts decide.
|
|
184
|
+
function evaluateKnownFacts(index, device, unknownFacts) {
|
|
185
|
+
const releasesById = new Map(index.releases.map(release => [release.id, release]));
|
|
186
|
+
const knownIndex = {
|
|
187
|
+
...index,
|
|
188
|
+
releases: index.releases.map(release => ({
|
|
189
|
+
...release,
|
|
190
|
+
conditions: release.conditions.filter(({ type }) => !unknownFacts.includes(type)),
|
|
191
|
+
rolloutPercent: unknownFacts.includes('rollout')
|
|
192
|
+
? 100
|
|
193
|
+
: release.rolloutPercent,
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
const { outcome, verdicts } = evaluateChannelIndex(knownIndex, device);
|
|
197
|
+
const resolveServedRelease = (release) => releasesById.get(release.id) ?? release;
|
|
198
|
+
return {
|
|
199
|
+
outcome: outcome.release === null
|
|
200
|
+
? outcome
|
|
201
|
+
: { ...outcome, release: resolveServedRelease(outcome.release) },
|
|
202
|
+
verdicts: verdicts.map(verdict => ({
|
|
203
|
+
...verdict,
|
|
204
|
+
release: resolveServedRelease(verdict.release),
|
|
205
|
+
})),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function resolveConditionsText(verdict, unknownFacts) {
|
|
209
|
+
const knownVerdicts = [...verdict.conditions];
|
|
210
|
+
const conditionTexts = verdict.release.conditions.map(({ type }) => {
|
|
211
|
+
if (unknownFacts.includes(type)) {
|
|
212
|
+
return `${type} unknown`;
|
|
213
|
+
}
|
|
214
|
+
const conditionVerdict = knownVerdicts.shift();
|
|
215
|
+
return `${type} ${conditionVerdict?.isSatisfied ? 'pass' : 'fail'}`;
|
|
216
|
+
});
|
|
217
|
+
return conditionTexts.join(', ') || 'none';
|
|
218
|
+
}
|
|
219
|
+
function resolveFailedBundleIds(events, index) {
|
|
220
|
+
const failedReleaseIds = new Set(events
|
|
221
|
+
.filter(({ type }) => type === 'failed')
|
|
222
|
+
.map(({ releaseId }) => releaseId));
|
|
223
|
+
return (index?.releases ?? [])
|
|
224
|
+
.filter(({ id }) => failedReleaseIds.has(id))
|
|
225
|
+
.map(({ bundleId }) => bundleId);
|
|
226
|
+
}
|
|
227
|
+
function resolveOutcomeText({ outcome }) {
|
|
228
|
+
const releaseText = outcome.release === null
|
|
229
|
+
? 'the embedded bundle'
|
|
230
|
+
: `release #${outcome.release.number}`;
|
|
231
|
+
switch (outcome.status) {
|
|
232
|
+
case 'available':
|
|
233
|
+
return `available, ${releaseText}${outcome.isMandatory ? ', mandatory' : ''}`;
|
|
234
|
+
case 'skipped':
|
|
235
|
+
return `skipped, ${outcome.reason}${outcome.conditionType === undefined ? '' : ` (${outcome.conditionType})`}, ${releaseText}`;
|
|
236
|
+
case 'up_to_date':
|
|
237
|
+
return `up_to_date, ${releaseText}`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// The index sequence is the publish time in milliseconds.
|
|
241
|
+
function resolveServedIndexText({ index, status, url }) {
|
|
242
|
+
return index === null
|
|
243
|
+
? `none: HTTP ${status} at ${url}; a device gets no release`
|
|
244
|
+
: `published ${new Date(index.sequence).toISOString()}`;
|
|
245
|
+
}
|
|
246
|
+
function resolveUnknownFacts(fetchedDevice, binary) {
|
|
247
|
+
if (fetchedDevice !== undefined) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
return binary?.fingerprint === undefined || binary.fingerprint === null
|
|
251
|
+
? ['fingerprint', 'os', 'rollout']
|
|
252
|
+
: ['os', 'rollout'];
|
|
253
|
+
}
|