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,134 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { defineCommand } from '../../utils/command.js';
|
|
4
|
+
import { InvalidParameterError } from '../../utils/errors.js';
|
|
5
|
+
import { defineCommandOptions } from '../../utils/global-options.js';
|
|
6
|
+
import { printJson, printTable } from '../../utils/output.js';
|
|
7
|
+
import { promptText } from '../../utils/prompts.js';
|
|
8
|
+
const FINGERPRINT_FILE_FIX = 'Write each file with "hotcodepush fingerprint --json > <file>", the earlier one at the commit of the store build.';
|
|
9
|
+
const FingerprintFileSchema = z.object({
|
|
10
|
+
fingerprint: z.string(),
|
|
11
|
+
extraFingerprintPaths: z.array(z.object({ path: z.string(), sha256: z.string() })),
|
|
12
|
+
packages: z.array(z.object({
|
|
13
|
+
integrity: z.string().nullable(),
|
|
14
|
+
name: z.string(),
|
|
15
|
+
version: z.string(),
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
18
|
+
export default defineCommand({
|
|
19
|
+
description: 'Print the packages and extra fingerprint paths added, removed or moved between <file-a> and <file-b>, two files written by fingerprint --json, the earlier first. Runs locally without a login.',
|
|
20
|
+
authentication: 'optional',
|
|
21
|
+
examples: [
|
|
22
|
+
'hotcodepush fingerprint diff store-build.json current.json',
|
|
23
|
+
'hotcodepush fingerprint diff store-build.json current.json --json',
|
|
24
|
+
],
|
|
25
|
+
args: z.array(z.string()),
|
|
26
|
+
options: defineCommandOptions({}),
|
|
27
|
+
action: async (options, args) => {
|
|
28
|
+
if (args.length > 2) {
|
|
29
|
+
throw new InvalidParameterError(`The fingerprint diff command compares two files, <file-a> and <file-b>. It does not take ${args.slice(2).join(' ')}.`);
|
|
30
|
+
}
|
|
31
|
+
const beforeFile = await readFingerprintFile('<file-a>', args[0] ??
|
|
32
|
+
(await promptText('<file-a>', "Which file holds the earlier fingerprint, the store build's?", options)));
|
|
33
|
+
const afterFile = await readFingerprintFile('<file-b>', args[1] ??
|
|
34
|
+
(await promptText('<file-b>', 'Which file holds the later fingerprint?', options)));
|
|
35
|
+
const diff = resolveFingerprintDiff(beforeFile, afterFile);
|
|
36
|
+
if (options.json) {
|
|
37
|
+
printJson(diff);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (diff.packages.length === 0 && diff.extraFingerprintPaths.length === 0) {
|
|
41
|
+
console.log(`Nothing moved: both files hold ${beforeFile.fingerprint}.`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
printTable({
|
|
45
|
+
emptyText: 'No package moved.',
|
|
46
|
+
headers: ['PACKAGE', 'BEFORE', 'AFTER'],
|
|
47
|
+
nextOffset: null,
|
|
48
|
+
rows: diff.packages.map(({ after, before, name }) => [
|
|
49
|
+
name,
|
|
50
|
+
resolveInstalledVersionText(before, after),
|
|
51
|
+
resolveInstalledVersionText(after, before),
|
|
52
|
+
]),
|
|
53
|
+
});
|
|
54
|
+
if (diff.extraFingerprintPaths.length > 0) {
|
|
55
|
+
printTable({
|
|
56
|
+
emptyText: '',
|
|
57
|
+
headers: ['EXTRA FINGERPRINT PATH', 'BEFORE', 'AFTER'],
|
|
58
|
+
nextOffset: null,
|
|
59
|
+
rows: diff.extraFingerprintPaths.map(({ after, before, path }) => [
|
|
60
|
+
path,
|
|
61
|
+
before ?? 'none',
|
|
62
|
+
after ?? 'none',
|
|
63
|
+
]),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
async function readFingerprintFile(argumentName, filePath) {
|
|
69
|
+
const text = await readFile(filePath, 'utf8').catch((error) => {
|
|
70
|
+
throw new InvalidParameterError(`The file ${filePath} from ${argumentName} cannot be read.`, FINGERPRINT_FILE_FIX, error);
|
|
71
|
+
});
|
|
72
|
+
const parsedFile = FingerprintFileSchema.safeParse(parseJson(text));
|
|
73
|
+
if (!parsedFile.success) {
|
|
74
|
+
throw new InvalidParameterError(`The file ${filePath} from ${argumentName} is not a fingerprint written by fingerprint --json.`, FINGERPRINT_FILE_FIX, parsedFile.error);
|
|
75
|
+
}
|
|
76
|
+
return parsedFile.data;
|
|
77
|
+
}
|
|
78
|
+
function parseJson(text) {
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(text);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function resolveFingerprintDiff(before, after) {
|
|
87
|
+
return {
|
|
88
|
+
extraFingerprintPaths: resolveExtraFingerprintPathChanges(before.extraFingerprintPaths, after.extraFingerprintPaths),
|
|
89
|
+
packages: resolvePackageChanges(before.packages, after.packages),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Pairs the versions in order: a pair moved, the rest was added or removed.
|
|
93
|
+
function resolvePackageChanges(before, after) {
|
|
94
|
+
const names = [...new Set([...before, ...after].map(({ name }) => name))];
|
|
95
|
+
return names.sort().flatMap(name => {
|
|
96
|
+
const beforeVersions = resolveVersionsOnlyIn(before, after, name);
|
|
97
|
+
const afterVersions = resolveVersionsOnlyIn(after, before, name);
|
|
98
|
+
return Array.from({ length: Math.max(beforeVersions.length, afterVersions.length) }, (_, index) => ({
|
|
99
|
+
after: afterVersions[index] ?? null,
|
|
100
|
+
before: beforeVersions[index] ?? null,
|
|
101
|
+
name,
|
|
102
|
+
}));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function resolveVersionsOnlyIn(packages, otherPackages, name) {
|
|
106
|
+
return packages
|
|
107
|
+
.filter(locked => locked.name === name &&
|
|
108
|
+
!otherPackages.some(other => other.name === name &&
|
|
109
|
+
other.version === locked.version &&
|
|
110
|
+
other.integrity === locked.integrity))
|
|
111
|
+
.map(({ integrity, version }) => ({ integrity, version }));
|
|
112
|
+
}
|
|
113
|
+
function resolveExtraFingerprintPathChanges(before, after) {
|
|
114
|
+
const beforeHashes = new Map(before.map(({ path, sha256 }) => [path, sha256]));
|
|
115
|
+
const afterHashes = new Map(after.map(({ path, sha256 }) => [path, sha256]));
|
|
116
|
+
const paths = [...new Set([...beforeHashes.keys(), ...afterHashes.keys()])];
|
|
117
|
+
return paths
|
|
118
|
+
.sort()
|
|
119
|
+
.map(path => ({
|
|
120
|
+
after: afterHashes.get(path) ?? null,
|
|
121
|
+
before: beforeHashes.get(path) ?? null,
|
|
122
|
+
path,
|
|
123
|
+
}))
|
|
124
|
+
.filter(({ after, before }) => after !== before);
|
|
125
|
+
}
|
|
126
|
+
// Shows the integrity where the version did not move, so a rebuild under the same version shows.
|
|
127
|
+
function resolveInstalledVersionText(installedVersion, otherInstalledVersion) {
|
|
128
|
+
if (installedVersion === null) {
|
|
129
|
+
return 'none';
|
|
130
|
+
}
|
|
131
|
+
return installedVersion.version === otherInstalledVersion?.version
|
|
132
|
+
? (installedVersion.integrity ?? 'no integrity')
|
|
133
|
+
: installedVersion.version;
|
|
134
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { computeFingerprint } from '@hotcodepush/protocol/fingerprint';
|
|
2
|
+
import { defineCommand } from '../utils/command.js';
|
|
3
|
+
import { readProjectFingerprintContributors } from '../utils/fingerprint.js';
|
|
4
|
+
import { defineCommandOptions } from '../utils/global-options.js';
|
|
5
|
+
import { printJson, printTable } from '../utils/output.js';
|
|
6
|
+
import { locateProjectConfig } from '../utils/project-config.js';
|
|
7
|
+
export default defineCommand({
|
|
8
|
+
description: "Print the project's native fingerprint with the packages and the files under the extra fingerprint paths that contribute to it. Runs locally without a login.",
|
|
9
|
+
authentication: 'optional',
|
|
10
|
+
examples: [
|
|
11
|
+
'hotcodepush fingerprint',
|
|
12
|
+
'hotcodepush fingerprint --json > fingerprint.json',
|
|
13
|
+
],
|
|
14
|
+
options: defineCommandOptions({}),
|
|
15
|
+
action: async (options) => {
|
|
16
|
+
const { directoryPath, projectConfig } = locateProjectConfig(options.config);
|
|
17
|
+
const contributors = await readProjectFingerprintContributors(directoryPath, projectConfig?.extraFingerprintPaths ?? []);
|
|
18
|
+
const fingerprint = computeFingerprint(contributors);
|
|
19
|
+
if (options.json) {
|
|
20
|
+
printJson({ fingerprint, ...contributors });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
console.log(`Fingerprint ${fingerprint}`);
|
|
24
|
+
printTable({
|
|
25
|
+
emptyText: 'No installed package ships native code.',
|
|
26
|
+
headers: ['PACKAGE', 'VERSION'],
|
|
27
|
+
nextOffset: null,
|
|
28
|
+
rows: contributors.packages.map(({ name, version }) => [name, version]),
|
|
29
|
+
});
|
|
30
|
+
if (contributors.extraFingerprintPaths.length > 0) {
|
|
31
|
+
printTable({
|
|
32
|
+
emptyText: '',
|
|
33
|
+
headers: ['EXTRA FINGERPRINT PATH', 'SHA-256'],
|
|
34
|
+
nextOffset: null,
|
|
35
|
+
rows: contributors.extraFingerprintPaths.map(({ path, sha256 }) => [
|
|
36
|
+
path,
|
|
37
|
+
sha256,
|
|
38
|
+
]),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { HotCodePushError } from '@hotcodepush/node';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { DOCS_URL, PROJECT_CONFIG_FILE_NAME, SIGNING_PRIVATE_KEY_FILE_NAME, } from '../config/consts.js';
|
|
6
|
+
import { createApiClient } from '../utils/api-client.js';
|
|
7
|
+
import { defineCommand } from '../utils/command.js';
|
|
8
|
+
import { fetchCurrentUser, isUnauthenticatedError, resolveCredentialText, } from '../utils/credential.js';
|
|
9
|
+
import { isInteractive } from '../utils/environment.js';
|
|
10
|
+
import { ConfirmationRequiredError, InvalidParameterError, MissingParameterError, NotLoggedInError, ReportedFailureError, UnexpectedError, } from '../utils/errors.js';
|
|
11
|
+
import { detectFramework } from '../utils/framework.js';
|
|
12
|
+
import { resolveFrameworkModule } from '../utils/frameworks/index.js';
|
|
13
|
+
import { defineCommandOptions } from '../utils/global-options.js';
|
|
14
|
+
import { resolveConsoleBaseUrl } from '../utils/hosts.js';
|
|
15
|
+
import { InitRun, resolveStepRows } from '../utils/init-steps.js';
|
|
16
|
+
import { stringifyLikeSource } from '../utils/json-file.js';
|
|
17
|
+
import { logIn } from '../utils/login.js';
|
|
18
|
+
import { printOutcomeRows } from '../utils/outcome.js';
|
|
19
|
+
import { printJson } from '../utils/output.js';
|
|
20
|
+
import { readPackageJson } from '../utils/package-json.js';
|
|
21
|
+
import { resolveCommandLineText, resolvePackageManager, resolveRunScriptCommandLine, runCommandLineVisibly, } from '../utils/package-manager.js';
|
|
22
|
+
import { ID_SCHEMA, locateProjectConfig, resolveProjectChannel, } from '../utils/project-config.js';
|
|
23
|
+
import { confirmConsequence, promptSelect, promptText, promptYesNo, } from '../utils/prompts.js';
|
|
24
|
+
import { fetchApps, fetchHasSigningKey, fetchOrganizations, } from '../utils/resource-resolution.js';
|
|
25
|
+
import { isEnvironmentToken, readToken } from '../utils/token-store.js';
|
|
26
|
+
import { readApiUrl } from '../utils/user-config.js';
|
|
27
|
+
import releaseCreateCommand from './release/create.js';
|
|
28
|
+
import signingKeyCreateCommand from './signing-key/create.js';
|
|
29
|
+
const CREATE_CHOICE = 'create';
|
|
30
|
+
export default defineCommand({
|
|
31
|
+
description: 'Set a React Native, Expo, Capacitor or Cordova project up from sign-in to the first release, re-runnable at any step.',
|
|
32
|
+
authentication: 'optional',
|
|
33
|
+
examples: ['hotcodepush init', 'hotcodepush init --yes --json'],
|
|
34
|
+
options: defineCommandOptions({
|
|
35
|
+
androidPath: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("The Android project, when it is not capacitor.config's android.path or android/."),
|
|
39
|
+
framework: z
|
|
40
|
+
.enum(['react_native', 'expo', 'capacitor', 'cordova'])
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('The framework, react_native, expo, capacitor or cordova, over the one detected from package.json.'),
|
|
43
|
+
iosPath: z
|
|
44
|
+
.string()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("The iOS project, when it is not capacitor.config's ios.path or ios/."),
|
|
47
|
+
xcodeTarget: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('The app target that gets the build step phase, when the iOS project has several.'),
|
|
51
|
+
}),
|
|
52
|
+
action: async (options) => {
|
|
53
|
+
const projectConfigLocation = locateProjectConfig(options.config);
|
|
54
|
+
const { directoryPath, projectConfig } = projectConfigLocation;
|
|
55
|
+
const framework = options.framework ?? detectFramework(directoryPath);
|
|
56
|
+
const frameworkModule = resolveFrameworkModule(framework);
|
|
57
|
+
const run = new InitRun();
|
|
58
|
+
const hotCodePush = await run.run('sign_in', () => signIn(options));
|
|
59
|
+
const scope = await run.run('organization', () => resolveOrganization(requireValue(hotCodePush), projectConfig, options), { dependsOn: ['sign_in'] });
|
|
60
|
+
const app = await run.run('app', () => resolveApp(requireValue(hotCodePush), requireValue(scope), projectConfigLocation, framework, options), { dependsOn: ['organization'] });
|
|
61
|
+
const projectFiles = await resolveProjectFiles(projectConfigLocation, frameworkModule, options);
|
|
62
|
+
const editBlocker = await resolveEditBlocker(projectFiles, app, options);
|
|
63
|
+
await run.run('package', () => installPackage(projectFiles, editBlocker));
|
|
64
|
+
await run.run('configuration', () => writeConfiguration(requireValue(hotCodePush), projectFiles, requireValue(app), editBlocker), { dependsOn: ['app'] });
|
|
65
|
+
await run.run('hook', () => projectFiles.wiring.wireBinaryCreateStep(editBlocker));
|
|
66
|
+
const privateKeyPath = await run.run('signing_key', () => createSigningKey(app, projectFiles, options));
|
|
67
|
+
const isReleaseWanted = run.stoppedCode === undefined &&
|
|
68
|
+
(await promptYesNo('Publish a first release now?', options));
|
|
69
|
+
const isBuilt = await run.run('build', () => buildProject(projectFiles, isReleaseWanted));
|
|
70
|
+
const isReleased = await run.run('release', () => releaseFirst(projectFiles, isReleaseWanted, isBuilt === true, privateKeyPath, options), { dependsOn: ['configuration', 'build'] });
|
|
71
|
+
const status = run.stoppedCode === undefined ? 'complete' : 'incomplete';
|
|
72
|
+
if (options.json) {
|
|
73
|
+
printJson({ status, steps: run.steps });
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// A blank line separates the rows from the package manager and build output above them.
|
|
77
|
+
console.log('');
|
|
78
|
+
printOutcomeRows(resolveStepRows(run.steps));
|
|
79
|
+
if (app !== undefined && status === 'complete') {
|
|
80
|
+
printFinish(app, isReleased === true, privateKeyPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (run.stoppedCode !== undefined) {
|
|
84
|
+
throw new ReportedFailureError(run.stoppedCode, run.stoppedStep);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
// A non-interactive login leaves the device code for the next run.
|
|
89
|
+
async function signIn(options) {
|
|
90
|
+
if (readToken() !== undefined) {
|
|
91
|
+
try {
|
|
92
|
+
return {
|
|
93
|
+
message: resolveCredentialText(await fetchCurrentUser(), isEnvironmentToken()),
|
|
94
|
+
status: 'skipped',
|
|
95
|
+
value: createApiClient(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!isUnauthenticatedError(error) &&
|
|
100
|
+
!(error instanceof NotLoggedInError)) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const user = await logIn(options);
|
|
106
|
+
return {
|
|
107
|
+
message: `Logged in as ${user.name} (${user.email}).`,
|
|
108
|
+
status: 'done',
|
|
109
|
+
value: createApiClient(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Without `--organization`, a person picks or creates one. Non-interactively the only one is used.
|
|
113
|
+
async function resolveOrganization(hotCodePush, projectConfig, options) {
|
|
114
|
+
const app = options.app === undefined && projectConfig?.appId !== undefined
|
|
115
|
+
? await fetchExistingApp(hotCodePush, projectConfig.appId)
|
|
116
|
+
: undefined;
|
|
117
|
+
if (app !== undefined) {
|
|
118
|
+
const organization = await hotCodePush.organizations.get({
|
|
119
|
+
organizationId: app.organizationId,
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
message: `Used organization ${organization.name}, the one of ${PROJECT_CONFIG_FILE_NAME}'s app.`,
|
|
123
|
+
status: 'done',
|
|
124
|
+
value: { app, organization },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const organizations = await fetchOrganizations(hotCodePush);
|
|
128
|
+
if (options.organization !== undefined) {
|
|
129
|
+
const named = findNamed(organizations, options.organization);
|
|
130
|
+
if (named !== undefined) {
|
|
131
|
+
return resolveUsedScope(named);
|
|
132
|
+
}
|
|
133
|
+
assertName('--organization', options.organization);
|
|
134
|
+
await confirmCreation('organization', options.organization, options);
|
|
135
|
+
return resolveCreatedScope(await hotCodePush.organizations.create({ name: options.organization }));
|
|
136
|
+
}
|
|
137
|
+
const [onlyOrganization] = organizations;
|
|
138
|
+
if (!isInteractive(options) &&
|
|
139
|
+
onlyOrganization !== undefined &&
|
|
140
|
+
organizations.length === 1) {
|
|
141
|
+
return resolveUsedScope(onlyOrganization);
|
|
142
|
+
}
|
|
143
|
+
const choice = await promptCreateOrPick('organization', organizations, options);
|
|
144
|
+
if (choice !== CREATE_CHOICE) {
|
|
145
|
+
return resolveUsedScope(requireById(organizations, choice));
|
|
146
|
+
}
|
|
147
|
+
const name = await promptText('--organization', 'What is the new organization called?', options);
|
|
148
|
+
return resolveCreatedScope(await hotCodePush.organizations.create({ name }));
|
|
149
|
+
}
|
|
150
|
+
// An app in the file that the API lacks is refused non-interactively. A person picks or creates another.
|
|
151
|
+
async function resolveApp(hotCodePush, { app, organization }, { filePath, projectConfig }, framework, options) {
|
|
152
|
+
if (app !== undefined) {
|
|
153
|
+
return {
|
|
154
|
+
message: `Used app ${app.name}, as ${PROJECT_CONFIG_FILE_NAME} names it.`,
|
|
155
|
+
status: 'done',
|
|
156
|
+
value: app,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (options.app === undefined &&
|
|
160
|
+
projectConfig?.appId !== undefined &&
|
|
161
|
+
!isInteractive(options)) {
|
|
162
|
+
throw new InvalidParameterError(`The app ${projectConfig.appId} in ${filePath ?? PROJECT_CONFIG_FILE_NAME} does not exist.`, `Pass --app with the app's id or name, or delete ${PROJECT_CONFIG_FILE_NAME} to create a new app.`);
|
|
163
|
+
}
|
|
164
|
+
const apps = await fetchApps(hotCodePush, organization.id);
|
|
165
|
+
if (options.app !== undefined) {
|
|
166
|
+
const named = findNamed(apps, options.app);
|
|
167
|
+
if (named !== undefined) {
|
|
168
|
+
return resolveUsed('app', named);
|
|
169
|
+
}
|
|
170
|
+
assertName('--app', options.app);
|
|
171
|
+
await confirmCreation('app', options.app, options);
|
|
172
|
+
return resolveCreated('app', await hotCodePush.organizations.apps.create({
|
|
173
|
+
framework,
|
|
174
|
+
name: options.app,
|
|
175
|
+
organizationId: organization.id,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
const [onlyApp] = apps;
|
|
179
|
+
if (!isInteractive(options) && onlyApp !== undefined && apps.length === 1) {
|
|
180
|
+
return resolveUsed('app', onlyApp);
|
|
181
|
+
}
|
|
182
|
+
const choice = await promptCreateOrPick('app', apps, options);
|
|
183
|
+
if (choice !== CREATE_CHOICE) {
|
|
184
|
+
return resolveUsed('app', requireById(apps, choice));
|
|
185
|
+
}
|
|
186
|
+
const name = await promptText('--app', 'What is the app called?', options);
|
|
187
|
+
return resolveCreated('app', await hotCodePush.organizations.apps.create({
|
|
188
|
+
framework,
|
|
189
|
+
name,
|
|
190
|
+
organizationId: organization.id,
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
async function resolveProjectFiles({ directoryPath, filePath, projectConfig }, framework, options) {
|
|
194
|
+
const packageJson = readPackageJson(directoryPath);
|
|
195
|
+
return {
|
|
196
|
+
configFilePath: filePath ?? join(directoryPath, PROJECT_CONFIG_FILE_NAME),
|
|
197
|
+
directoryPath,
|
|
198
|
+
framework,
|
|
199
|
+
packageJson,
|
|
200
|
+
projectConfig,
|
|
201
|
+
wiring: await framework.resolveWiring({ directoryPath, packageJson }, options),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Declined, or with nobody to ask, each editing step stops with that reason.
|
|
205
|
+
async function resolveEditBlocker(projectFiles, app, options) {
|
|
206
|
+
const filePaths = resolveFilesToChange(projectFiles, app);
|
|
207
|
+
if (filePaths.length === 0) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
const consequence = `changes ${filePaths.join(', ')}`;
|
|
211
|
+
const manualStep = `Run "hotcodepush init --yes" to change ${filePaths.join(', ')}.`;
|
|
212
|
+
try {
|
|
213
|
+
return (await confirmConsequence(consequence, options))
|
|
214
|
+
? undefined
|
|
215
|
+
: new ConfirmationRequiredError(consequence, manualStep);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (error instanceof ConfirmationRequiredError) {
|
|
219
|
+
return new ConfirmationRequiredError(consequence, manualStep);
|
|
220
|
+
}
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Without an app the configuration step waits, so `hotcodepush.json` is not among the files.
|
|
225
|
+
function resolveFilesToChange({ configFilePath, projectConfig, wiring }, app) {
|
|
226
|
+
return [
|
|
227
|
+
...wiring.packageFilePaths,
|
|
228
|
+
...(app === undefined || isConfigurationComplete(projectConfig, app)
|
|
229
|
+
? []
|
|
230
|
+
: [basename(configFilePath)]),
|
|
231
|
+
...wiring.nativeFilePaths,
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
function installPackage({ framework, wiring }, editBlocker) {
|
|
235
|
+
if (wiring.isPackageInstalled) {
|
|
236
|
+
return Promise.resolve({
|
|
237
|
+
message: `The package ${framework.packageName} is already installed.`,
|
|
238
|
+
status: 'skipped',
|
|
239
|
+
value: undefined,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (editBlocker !== undefined) {
|
|
243
|
+
throw editBlocker;
|
|
244
|
+
}
|
|
245
|
+
return Promise.resolve({
|
|
246
|
+
message: wiring.installPackage(),
|
|
247
|
+
status: 'done',
|
|
248
|
+
value: undefined,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async function writeConfiguration(hotCodePush, { configFilePath, projectConfig }, app, editBlocker) {
|
|
252
|
+
if (isConfigurationComplete(projectConfig, app)) {
|
|
253
|
+
return {
|
|
254
|
+
message: `The file ${basename(configFilePath)} is already present.`,
|
|
255
|
+
status: 'skipped',
|
|
256
|
+
value: undefined,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (editBlocker !== undefined) {
|
|
260
|
+
throw editBlocker;
|
|
261
|
+
}
|
|
262
|
+
const sourceText = existsSync(configFilePath)
|
|
263
|
+
? readFileSync(configFilePath, 'utf8')
|
|
264
|
+
: '';
|
|
265
|
+
writeFileSync(configFilePath, stringifyLikeSource({
|
|
266
|
+
...projectConfig,
|
|
267
|
+
appId: app.id,
|
|
268
|
+
...(projectConfig?.channel !== undefined
|
|
269
|
+
? {}
|
|
270
|
+
: { channel: await fetchDefaultChannelName(hotCodePush, app) }),
|
|
271
|
+
}, sourceText || '{}\n'));
|
|
272
|
+
return {
|
|
273
|
+
message: resolveConfigurationMessage(basename(configFilePath), projectConfig, app),
|
|
274
|
+
status: 'done',
|
|
275
|
+
value: undefined,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function resolveConfigurationMessage(fileName, projectConfig, app) {
|
|
279
|
+
if (projectConfig === undefined) {
|
|
280
|
+
return `Wrote ${fileName}.`;
|
|
281
|
+
}
|
|
282
|
+
if (projectConfig.appId !== undefined && projectConfig.appId !== app.id) {
|
|
283
|
+
return `Set the app in ${fileName} to ${app.name}.`;
|
|
284
|
+
}
|
|
285
|
+
return `Completed ${fileName}.`;
|
|
286
|
+
}
|
|
287
|
+
async function fetchDefaultChannelName(hotCodePush, app) {
|
|
288
|
+
if (app.defaultChannelId === null) {
|
|
289
|
+
return resolveProjectChannel({});
|
|
290
|
+
}
|
|
291
|
+
const defaultChannel = await hotCodePush.apps.channels.get({
|
|
292
|
+
appId: app.id,
|
|
293
|
+
channelId: app.defaultChannelId,
|
|
294
|
+
});
|
|
295
|
+
return defaultChannel.name;
|
|
296
|
+
}
|
|
297
|
+
// Nobody to ask points at the command instead, since a key file written on a runner is lost with it.
|
|
298
|
+
async function createSigningKey(app, { configFilePath }, options) {
|
|
299
|
+
if (app !== undefined &&
|
|
300
|
+
(await fetchHasSigningKey(createApiClient(), app.id))) {
|
|
301
|
+
return {
|
|
302
|
+
message: 'The app already has a signing key.',
|
|
303
|
+
status: 'skipped',
|
|
304
|
+
value: undefined,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (app === undefined ||
|
|
308
|
+
!(await promptYesNo('Generate a signing key pair?', options))) {
|
|
309
|
+
return {
|
|
310
|
+
message: 'Run "hotcodepush signing-key create" to enable code signing.',
|
|
311
|
+
status: 'skipped',
|
|
312
|
+
value: undefined,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const privateKeyPath = SIGNING_PRIVATE_KEY_FILE_NAME;
|
|
316
|
+
await signingKeyCreateCommand.action({
|
|
317
|
+
app: app.id,
|
|
318
|
+
config: existsSync(configFilePath) ? configFilePath : undefined,
|
|
319
|
+
json: options.json,
|
|
320
|
+
privateKeyPath,
|
|
321
|
+
}, undefined);
|
|
322
|
+
return {
|
|
323
|
+
message: 'Generated a signing key pair, its private key in the file named above.',
|
|
324
|
+
status: 'done',
|
|
325
|
+
value: privateKeyPath,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function buildProject({ directoryPath, framework, packageJson }, isReleaseWanted) {
|
|
329
|
+
if (framework.packageBundles !== undefined) {
|
|
330
|
+
return Promise.resolve({
|
|
331
|
+
message: 'The command "hotcodepush release create" packages the bundles itself.',
|
|
332
|
+
status: 'skipped',
|
|
333
|
+
value: false,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const buildCommandLine = resolveBuildCommandLine(directoryPath, packageJson);
|
|
337
|
+
if (buildCommandLine === undefined) {
|
|
338
|
+
return Promise.resolve({
|
|
339
|
+
message: 'There is no build script in package.json.',
|
|
340
|
+
status: 'skipped',
|
|
341
|
+
value: false,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (!isReleaseWanted) {
|
|
345
|
+
return Promise.resolve({
|
|
346
|
+
message: 'No release follows.',
|
|
347
|
+
status: 'skipped',
|
|
348
|
+
value: false,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
runCommandLineVisibly(buildCommandLine, directoryPath);
|
|
352
|
+
return Promise.resolve({
|
|
353
|
+
message: `Ran "${resolveCommandLineText(buildCommandLine)}".`,
|
|
354
|
+
status: 'done',
|
|
355
|
+
value: true,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
async function releaseFirst({ configFilePath, directoryPath, framework, packageJson }, isReleaseWanted, isBuilt, privateKeyPath, options) {
|
|
359
|
+
const releaseCommandText = resolveReleaseCommandText(privateKeyPath);
|
|
360
|
+
if (!isReleaseWanted) {
|
|
361
|
+
const buildCommandLine = framework.packageBundles === undefined
|
|
362
|
+
? resolveBuildCommandLine(directoryPath, packageJson)
|
|
363
|
+
: undefined;
|
|
364
|
+
return {
|
|
365
|
+
message: buildCommandLine === undefined
|
|
366
|
+
? `Run "${releaseCommandText}" to publish the first release.`
|
|
367
|
+
: `Run "${resolveCommandLineText(buildCommandLine)}", then "${releaseCommandText}" to publish the first release.`,
|
|
368
|
+
status: 'skipped',
|
|
369
|
+
value: false,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const buildDirectory = framework.readBuildDirectory(directoryPath);
|
|
373
|
+
if (framework.packageBundles === undefined &&
|
|
374
|
+
!isBuilt &&
|
|
375
|
+
'path' in buildDirectory &&
|
|
376
|
+
!existsSync(join(directoryPath, buildDirectory.path))) {
|
|
377
|
+
const buildCommandLine = resolveBuildCommandLine(directoryPath, packageJson);
|
|
378
|
+
return {
|
|
379
|
+
message: `There is ${buildCommandLine === undefined ? 'no build script and ' : ''}no output at ${buildDirectory.path}. Build, then run "${releaseCommandText}".`,
|
|
380
|
+
status: 'skipped',
|
|
381
|
+
value: false,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
await releaseCreateCommand.action({
|
|
385
|
+
config: configFilePath,
|
|
386
|
+
json: options.json,
|
|
387
|
+
privateKeyPath,
|
|
388
|
+
yes: true,
|
|
389
|
+
}, undefined);
|
|
390
|
+
return {
|
|
391
|
+
message: 'Released to production.',
|
|
392
|
+
status: 'done',
|
|
393
|
+
value: true,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// A skipped release row already names the release command, so it is repeated only after a release.
|
|
397
|
+
function printFinish(app, isReleased, privateKeyPath) {
|
|
398
|
+
console.log('');
|
|
399
|
+
console.log(isReleased
|
|
400
|
+
? `Build the app natively once and run it. Change a line, run the build and "${resolveReleaseCommandText(privateKeyPath)}", then reopen the app.`
|
|
401
|
+
: 'Build the app natively once and run it, then publish the first release as shown above.');
|
|
402
|
+
console.log(`Console: ${resolveConsoleBaseUrl(readApiUrl())}/apps/${app.id}`);
|
|
403
|
+
console.log(`Docs: ${DOCS_URL}`);
|
|
404
|
+
}
|
|
405
|
+
function assertName(flag, reference) {
|
|
406
|
+
if (ID_SCHEMA.safeParse(reference).success) {
|
|
407
|
+
throw new InvalidParameterError(`Nothing has the id ${reference} from ${flag}.`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function confirmCreation(noun, name, options) {
|
|
411
|
+
const isConfirmed = await confirmConsequence(`creates the ${noun} ${name}, which does not exist yet`, options);
|
|
412
|
+
if (!isConfirmed) {
|
|
413
|
+
throw new MissingParameterError(`--${noun}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Such as a copied project, a cloned demo or a reset environment.
|
|
417
|
+
async function fetchExistingApp(hotCodePush, appId) {
|
|
418
|
+
try {
|
|
419
|
+
return await hotCodePush.apps.get({ appId });
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (error instanceof HotCodePushError && error.code === 'not_found') {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function findNamed(resources, reference) {
|
|
429
|
+
return ID_SCHEMA.safeParse(reference).success
|
|
430
|
+
? resources.find(({ id }) => id === reference)
|
|
431
|
+
: resources.find(({ name }) => name.toLowerCase() === reference.toLowerCase());
|
|
432
|
+
}
|
|
433
|
+
function isConfigurationComplete(projectConfig, app) {
|
|
434
|
+
return projectConfig?.appId === app.id && projectConfig.channel !== undefined;
|
|
435
|
+
}
|
|
436
|
+
function promptCreateOrPick(noun, resources, options) {
|
|
437
|
+
return promptSelect(`--${noun}`, `Which ${noun}?`, [
|
|
438
|
+
...resources.map(({ id, name }) => ({ label: name, value: id })),
|
|
439
|
+
{ label: `Create a new ${noun}`, value: CREATE_CHOICE },
|
|
440
|
+
], options);
|
|
441
|
+
}
|
|
442
|
+
function requireById(resources, id) {
|
|
443
|
+
const resource = resources.find(candidate => candidate.id === id);
|
|
444
|
+
if (resource === undefined) {
|
|
445
|
+
throw new InvalidParameterError(`Nothing has the id ${id}.`);
|
|
446
|
+
}
|
|
447
|
+
return resource;
|
|
448
|
+
}
|
|
449
|
+
// A stopped step skips the rest, so the run never gets here without the value.
|
|
450
|
+
function requireValue(value) {
|
|
451
|
+
if (value === undefined) {
|
|
452
|
+
throw new UnexpectedError(new Error('An earlier step did not run.'));
|
|
453
|
+
}
|
|
454
|
+
return value;
|
|
455
|
+
}
|
|
456
|
+
function resolveBuildCommandLine(directoryPath, packageJson) {
|
|
457
|
+
return packageJson.scripts?.build === undefined
|
|
458
|
+
? undefined
|
|
459
|
+
: resolveRunScriptCommandLine(resolvePackageManager(directoryPath), 'build');
|
|
460
|
+
}
|
|
461
|
+
function resolveReleaseCommandText(privateKeyPath) {
|
|
462
|
+
return privateKeyPath === undefined
|
|
463
|
+
? 'hotcodepush release create'
|
|
464
|
+
: `hotcodepush release create --private-key-path ${privateKeyPath}`;
|
|
465
|
+
}
|
|
466
|
+
function resolveCreatedScope(organization) {
|
|
467
|
+
return {
|
|
468
|
+
message: `Created organization ${organization.name}.`,
|
|
469
|
+
status: 'done',
|
|
470
|
+
value: { app: undefined, organization },
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function resolveUsedScope(organization) {
|
|
474
|
+
return {
|
|
475
|
+
message: `Used organization ${organization.name}.`,
|
|
476
|
+
status: 'done',
|
|
477
|
+
value: { app: undefined, organization },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function resolveCreated(noun, resource) {
|
|
481
|
+
return {
|
|
482
|
+
message: `Created ${noun} ${resource.name}.`,
|
|
483
|
+
status: 'done',
|
|
484
|
+
value: resource,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function resolveUsed(noun, resource) {
|
|
488
|
+
return {
|
|
489
|
+
message: `Used ${noun} ${resource.name}.`,
|
|
490
|
+
status: 'done',
|
|
491
|
+
value: resource,
|
|
492
|
+
};
|
|
493
|
+
}
|