-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy pathDevvitCommand.ts
More file actions
209 lines (183 loc) · 6.87 KB
/
DevvitCommand.ts
File metadata and controls
209 lines (183 loc) · 6.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import type { T2 } from '@devvit/shared-types/tid.js';
import { Command, Flags } from '@oclif/core';
import { parse } from '@oclif/core/lib/parser/index.js';
import inquirer from 'inquirer';
import open from 'open';
import type { StoredToken } from '../../lib/auth/StoredToken.js';
import { getAccessToken } from '../auth.js';
import { createDeveloperAccountClient } from '../clientGenerators.js';
import { DEVVIT_PORTAL_URL } from '../config.js';
import {
type BuildMode,
devvitClassicConfigFilename,
devvitV1ConfigFilename,
newProject,
type Project,
} from '../project.js';
import { fetchUserDisplayName, fetchUserT2Id } from '../r2Api/user.js';
/**
* Note: we have to return `Promise<string>` here rather than just `string`
* The official documentation has an error and doesn't match the TS declarations for this method
*
* @see https://oclif.io/docs/args/
*/
export const toLowerCaseArgParser = async (input: string): Promise<string> => input.toLowerCase();
export abstract class DevvitCommand extends Command {
static override baseFlags = {
config: Flags.string({ description: 'path to devvit config file' }),
} as const;
protected readonly developerAccountClient = createDeveloperAccountClient();
#project: Project | undefined;
/** Project configuration. */
get project(): Project {
if (!this.#project) this.error(`No project ${devvitClassicConfigFilename} config file found.`);
return this.#project;
}
/**
* Warning: mutating project state is not well supported. State copies are not
* invalidated.
*/
set project(project: Project) {
this.#project = project;
}
isRunningInAppDirectory(): boolean {
return this.#project !== undefined;
}
protected override async init(mode?: BuildMode | 'None'): Promise<void> {
await super.init();
// to-do: avoid subclassing and compose instead. subclasses cause bugs
// because of all the inherited behavior wanted or not, require
// understanding the entire hierarchy top to bottom (and left to
// right for a base class like this), and need crazy hacks like
// below.
const baseFlags = Object.keys(DevvitCommand.baseFlags).map((flag) => `--${flag}`);
const baseArgv = this.argv.filter(
(arg) => !arg.startsWith('--') || baseFlags.some((flag) => arg.startsWith(flag))
);
// call parse() instead of this.parse() which only knows of
// DevvitCommand.baseFlags.
const { flags } = await parse(baseArgv, {
strict: false,
flags: DevvitCommand.baseFlags,
});
// If we're in 'None' mode, we don't need to initialize a project, and in fact shouldn't try.
if (mode !== 'None') {
this.#project = await newProject(flags.config, mode ?? 'Static');
if (flags.config && !this.#project) this.error(`Project config "${flags.config}" not found.`);
if (
flags.config &&
flags.config !== devvitClassicConfigFilename &&
flags.config !== devvitV1ConfigFilename
) {
this.log(`Using custom config file: ${flags.config}`);
}
}
}
protected checkDeveloperAccount = async (): Promise<void> => {
const { acceptedTermsVersion, currentTermsVersion } =
await this.developerAccountClient.GetUserAccountInfoIfExists({});
const devAccountUrl = `${DEVVIT_PORTAL_URL}/create-account?cli=true`;
if (acceptedTermsVersion < currentTermsVersion) {
this.log('Please finish setting up your developer account before proceeding:');
this.log(devAccountUrl);
type ActionType = 'open' | 'tryAgain' | 'exit';
const { action } = await inquirer.prompt<{ action: ActionType }>({
name: 'action',
message: 'What would you like to do?',
type: 'list',
choices: [
{
name: 'Open developer account page in browser',
value: 'open',
},
{
name: 'I have finished setting up my developer account; check again',
value: 'tryAgain',
},
{
name: 'Exit',
value: 'exit',
},
],
});
if (action === 'open') {
try {
await open(devAccountUrl);
} catch {
this.error(
'An error occurred when trying to open the developer account page. Please try again.'
);
}
return this.checkDeveloperAccount();
}
if (action === 'tryAgain') {
return this.checkDeveloperAccount();
}
if (action === 'exit') {
process.exit(1);
}
this.error('Invalid action; quitting');
}
};
protected async checkIfUserLoggedIn(): Promise<void> {
const token = await getAccessToken();
if (!token) {
this.error('Not currently logged in. Try `devvit login` first');
}
}
/**
* @description Get the user's display name from the stored token.
*/
protected async getUserDisplayName(token: StoredToken): Promise<string> {
const res = await fetchUserDisplayName(token);
if (!res.ok) {
this.error(`${res.error}. Try again or re-login with \`devvit login\`.`);
}
return res.value;
}
/**
* @description Get the user's t2 id from the stored token.
*/
protected async getUserT2Id(token: StoredToken): Promise<T2> {
const res = await fetchUserT2Id(token);
if (!res.ok) {
this.error(`${res.error}. Try again or re-login with \`devvit login\`.`);
}
return res.value;
}
/**
* @description Handle resolving the appname@version for the following cases
*
* Case 1: devvit <publish|install> <app-name>@<version> - can be run anywhere
* Case 1: devvit <publish|install> <app-name> - can be run anywhere
* Case 3: devvit <publish|install> @<version> - must be in project directory
* Case 2: devvit <publish|install> - must be in project directory
*/
protected async inferAppNameAndVersion(
appWithVersion: string | undefined
): Promise<{ appName: string; version: string }> {
appWithVersion = appWithVersion ?? '';
// If the agrument has "<app-name>@<version>" format, then both appName and version can be inferred
if (appWithVersion.includes('@') && !appWithVersion.startsWith('@')) {
const [appName, version] = appWithVersion.split('@');
return {
appName,
version,
};
}
// If the agrument has "<app-name>" format, then we assume the version as "latest"
if (appWithVersion.length > 0 && !appWithVersion.includes('@')) {
return {
appName: appWithVersion,
version: 'latest',
};
}
// Otherwise, we need to read appName or app version from the config
// If the agrument has "@<version>" format
if (appWithVersion.startsWith('@')) {
return { appName: this.project.name, version: appWithVersion.slice(1) };
}
// Otherwise, default to the config and latest.
return { appName: this.project.name, version: 'latest' };
}
}