@swell/cli 1.0.2 → 2.0.1-alpha.2
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/LICENSE.md +21 -0
- package/README.md +2 -140
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +7 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +7 -0
- package/dist/app-command.d.ts +22 -0
- package/dist/app-command.js +45 -0
- package/dist/commands/app/_pull.d.ts +8 -0
- package/dist/commands/app/_pull.js +45 -0
- package/dist/commands/app/_uninstall.d.ts +12 -0
- package/dist/commands/app/_uninstall.js +45 -0
- package/dist/commands/app/info.d.ts +15 -0
- package/dist/commands/app/info.js +92 -0
- package/dist/commands/app/install.d.ts +17 -0
- package/dist/commands/app/install.js +177 -0
- package/dist/commands/app/push.d.ts +17 -0
- package/dist/commands/app/push.js +158 -0
- package/dist/commands/app/version.d.ts +18 -0
- package/dist/commands/app/version.js +156 -0
- package/dist/commands/app/watch.d.ts +15 -0
- package/dist/commands/app/watch.js +110 -0
- package/dist/commands/create/app.d.ts +21 -0
- package/dist/commands/create/app.js +210 -0
- package/dist/commands/create/content.d.ts +19 -0
- package/dist/commands/create/content.js +87 -0
- package/dist/commands/create/function.d.ts +25 -0
- package/dist/commands/create/function.js +191 -0
- package/dist/commands/create/index.d.ts +6 -0
- package/dist/commands/create/index.js +34 -0
- package/dist/commands/create/model.d.ts +19 -0
- package/dist/commands/create/model.js +87 -0
- package/dist/commands/create/notification.d.ts +25 -0
- package/dist/commands/create/notification.js +170 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +77 -0
- package/dist/commands/logout.d.ts +8 -0
- package/dist/commands/logout.js +45 -0
- package/dist/commands/logs.d.ts +23 -0
- package/dist/commands/logs.js +207 -0
- package/dist/commands/switch.d.ts +10 -0
- package/dist/commands/switch.js +41 -0
- package/dist/commands/whoami.d.ts +7 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/create-collection-command.d.ts +26 -0
- package/dist/create-collection-command.js +97 -0
- package/dist/create-command.d.ts +16 -0
- package/dist/create-command.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +117 -0
- package/dist/lib/app-config.d.ts +29 -0
- package/dist/lib/app-config.js +127 -0
- package/dist/lib/apps/app-config.d.ts +42 -0
- package/dist/lib/apps/app-config.js +167 -0
- package/dist/lib/apps/index.d.ts +67 -0
- package/dist/lib/apps/index.js +167 -0
- package/dist/lib/apps/paths.d.ts +18 -0
- package/dist/lib/apps/paths.js +54 -0
- package/dist/lib/bundle.d.ts +1 -0
- package/dist/lib/bundle.js +40 -0
- package/dist/lib/config.d.ts +37 -0
- package/dist/lib/config.js +131 -0
- package/dist/lib/constants.d.ts +5 -0
- package/dist/lib/constants.js +17 -0
- package/dist/lib/create/content.d.ts +23 -0
- package/dist/lib/create/content.js +40 -0
- package/dist/lib/create/function.d.ts +17 -0
- package/dist/lib/create/function.js +36 -0
- package/dist/lib/create/index.d.ts +13 -0
- package/dist/lib/create/index.js +20 -0
- package/dist/lib/create/model.d.ts +22 -0
- package/dist/lib/create/model.js +30 -0
- package/dist/lib/create/notification.d.ts +25 -0
- package/dist/lib/create/notification.js +1 -0
- package/dist/lib/info.d.ts +38 -0
- package/dist/lib/info.js +91 -0
- package/dist/lib/logs/index.d.ts +23 -0
- package/dist/lib/logs/index.js +88 -0
- package/dist/lib/logs/line-output.d.ts +5 -0
- package/dist/lib/logs/line-output.js +13 -0
- package/dist/lib/logs/output.d.ts +7 -0
- package/dist/lib/logs/output.js +8 -0
- package/dist/lib/logs/table-output.d.ts +11 -0
- package/dist/lib/logs/table-output.js +128 -0
- package/dist/lib/sessions.d.ts +9 -0
- package/dist/lib/sessions.js +110 -0
- package/dist/lib/stores.d.ts +63 -0
- package/dist/lib/stores.js +173 -0
- package/dist/lib/style.d.ts +24 -0
- package/dist/lib/style.js +29 -0
- package/dist/lib/swell-function-wrapper.d.ts +88 -0
- package/dist/lib/swell-function-wrapper.js +302 -0
- package/dist/remote-app-command.d.ts +36 -0
- package/dist/remote-app-command.js +367 -0
- package/dist/swell-command.d.ts +21 -0
- package/dist/swell-command.js +43 -0
- package/oclif.manifest.json +1085 -0
- package/package.json +81 -43
- package/LICENSE +0 -674
- package/dist/bin/multipart-parser-85ef9626.js +0 -456
- package/dist/bin/swell-0f71f07d.js +0 -30670
- package/dist/bin/swell.js +0 -24
- package/dist/man/swell.1 +0 -85
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import * as semver from 'semver';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import config from '../../lib/config.js';
|
|
6
|
+
import style from '../../lib/style.js';
|
|
7
|
+
import { selectLoggedInStoreId, selectEnvironmentId, } from '../../lib/stores.js';
|
|
8
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
9
|
+
export default class AppInstall extends RemoteAppCommand {
|
|
10
|
+
static description = `Without a version specified, install the latest version of the app.
|
|
11
|
+
|
|
12
|
+
Without a store specified, install the app in the Live environment of the current store.`;
|
|
13
|
+
static examples = [
|
|
14
|
+
'swell install',
|
|
15
|
+
'swell install -v 3.2.7',
|
|
16
|
+
'swell install -v 1.0.0 -s my-store-id',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
env: Flags.string({
|
|
20
|
+
char: 'e',
|
|
21
|
+
description: 'target environment to install the app, defaults to live',
|
|
22
|
+
}),
|
|
23
|
+
store: Flags.string({
|
|
24
|
+
char: 's',
|
|
25
|
+
description: 'target store id to install the app, defaults to the current store',
|
|
26
|
+
}),
|
|
27
|
+
version: Flags.string({
|
|
28
|
+
char: 'v',
|
|
29
|
+
description: 'version of the app to install, defaults to latest',
|
|
30
|
+
async parse(input) {
|
|
31
|
+
if (!semver.valid(input)) {
|
|
32
|
+
throw new Error(`Invalid version: ${input}`);
|
|
33
|
+
}
|
|
34
|
+
return input;
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
static summary = 'Install an existing app in another store environment.';
|
|
39
|
+
async run() {
|
|
40
|
+
const { flags } = await this.parse(AppInstall);
|
|
41
|
+
const { env, store, version } = flags;
|
|
42
|
+
this.app = await this.getAppWithConfig();
|
|
43
|
+
const versions = await this.getVersions(version);
|
|
44
|
+
if (versions.length === 0) {
|
|
45
|
+
this.error(`${version ? `Version ${version} not found` : 'No versions found'} for ${style.appConfigValue(this.app.name)} app. ${version
|
|
46
|
+
? ''
|
|
47
|
+
: `Create a version with the ${style.command('swell version')} command first.`}`);
|
|
48
|
+
}
|
|
49
|
+
const versionToInstall = version
|
|
50
|
+
? versions.find((ver) => ver.version === version)?.version
|
|
51
|
+
: versions[0].version;
|
|
52
|
+
let storeToInstall = store;
|
|
53
|
+
if (!storeToInstall) {
|
|
54
|
+
const loggedInStores = config.get('stores') || [];
|
|
55
|
+
if (loggedInStores.length > 0) {
|
|
56
|
+
storeToInstall = await selectLoggedInStoreId('Which store do you want to install the app in?');
|
|
57
|
+
}
|
|
58
|
+
if (!storeToInstall) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
let envToInstall = env;
|
|
63
|
+
if (!envToInstall) {
|
|
64
|
+
envToInstall = await selectEnvironmentId();
|
|
65
|
+
}
|
|
66
|
+
if (envToInstall === 'live') {
|
|
67
|
+
envToInstall = undefined;
|
|
68
|
+
}
|
|
69
|
+
if (envToInstall && envToInstall !== 'test') {
|
|
70
|
+
this.error(`${envToInstall} is not a valid environment.`);
|
|
71
|
+
}
|
|
72
|
+
if (envToInstall === 'test' && storeToInstall === this.app.client_id) {
|
|
73
|
+
this.error(`You cannot install an app in the test environment of the same store.`);
|
|
74
|
+
}
|
|
75
|
+
config.ensureLoggedIn(storeToInstall);
|
|
76
|
+
this.api.setStoreEnv(storeToInstall, envToInstall);
|
|
77
|
+
// check if app is already installed on store
|
|
78
|
+
const existingInstalledApp = await this.getInstalledApp();
|
|
79
|
+
const existingVersion = existingInstalledApp?.id
|
|
80
|
+
? existingInstalledApp.version
|
|
81
|
+
: null;
|
|
82
|
+
// confirm install unless params were passed explicitly
|
|
83
|
+
let extraMessage = '';
|
|
84
|
+
if (!store || !env) {
|
|
85
|
+
const continueInstall = await confirm({
|
|
86
|
+
message: this.installConfirmationMessage(storeToInstall, envToInstall, versionToInstall, existingVersion),
|
|
87
|
+
});
|
|
88
|
+
if (!continueInstall) {
|
|
89
|
+
// we don't want to throw an error as it is a valid exit
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
extraMessage = ` ${style.appConfigValue(this.app.name)} v${versionToInstall} on ${style.storeId(storeToInstall)} [${envToInstall ? envToInstall : 'live'}]`;
|
|
95
|
+
}
|
|
96
|
+
const shouldContinue = await this.confirmRemoveDevelopmentApp(storeToInstall);
|
|
97
|
+
if (!shouldContinue) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const spinner = ora();
|
|
101
|
+
// install or update app depending on whether it's already installed
|
|
102
|
+
let request;
|
|
103
|
+
if (existingInstalledApp?.id) {
|
|
104
|
+
spinner.start(`Updating${extraMessage}...`);
|
|
105
|
+
request = async () => this.updateApp(versionToInstall, envToInstall);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
spinner.start(`Installing${extraMessage}...`);
|
|
109
|
+
request = async () => this.installApp(versionToInstall, envToInstall);
|
|
110
|
+
}
|
|
111
|
+
const response = await this.handleRequestErrors(request, () => spinner.fail('Something went wrong.'));
|
|
112
|
+
if (response?.errors) {
|
|
113
|
+
spinner.fail(`Error installing app: ${JSON.stringify(response.errors)}`);
|
|
114
|
+
}
|
|
115
|
+
spinner.succeed(`Install complete. Manage app settings in your dashboard at ${style.link(this.dashboardUrl({ flags: { env: envToInstall } }, storeToInstall))}.`);
|
|
116
|
+
}
|
|
117
|
+
async installApp(version, env) {
|
|
118
|
+
return this.api.post({ adminPath: `/client/apps` },
|
|
119
|
+
// eslint-disable-next-line camelcase
|
|
120
|
+
{ body: { app_id: this.app.id, version } });
|
|
121
|
+
}
|
|
122
|
+
installConfirmationMessage(storeToInstall, envToInstall, versionToInstall, existingVersion) {
|
|
123
|
+
const message = [];
|
|
124
|
+
if (existingVersion && semver.gt(versionToInstall, existingVersion)) {
|
|
125
|
+
message.push('Updating');
|
|
126
|
+
}
|
|
127
|
+
else if (existingVersion &&
|
|
128
|
+
semver.lt(versionToInstall, existingVersion)) {
|
|
129
|
+
message.push('Rolling back');
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
message.push('Install');
|
|
133
|
+
}
|
|
134
|
+
let versionToShow = versionToInstall;
|
|
135
|
+
versionToShow += versionToInstall === this.app.version ? ' (latest)' : '';
|
|
136
|
+
message.push(style.appConfigValue(this.app.name), `v${versionToShow}`, `on ${style.storeId(storeToInstall)} [${envToInstall ? envToInstall : 'live'}]?`);
|
|
137
|
+
return message.join(' ');
|
|
138
|
+
}
|
|
139
|
+
async updateApp(version, env) {
|
|
140
|
+
await this.api.put({ adminPath: `/client/apps/${this.app.id}` }, { body: { version } });
|
|
141
|
+
}
|
|
142
|
+
async getInstalledApp() {
|
|
143
|
+
// check if app is already installed on store
|
|
144
|
+
return await this.api.get({ adminPath: `/client/apps/${this.app.id}` }, {});
|
|
145
|
+
}
|
|
146
|
+
async confirmRemoveDevelopmentApp(storeToInstall) {
|
|
147
|
+
const existingSourcedApps = await this.api.get({ adminPath: `/client/apps` }, {
|
|
148
|
+
query: { source_id: this.app.id },
|
|
149
|
+
});
|
|
150
|
+
if (existingSourcedApps.results[0]?.id) {
|
|
151
|
+
const continueInstall = await confirm({
|
|
152
|
+
message: `${style.storeId(storeToInstall)} has a development version of ${style.appConfigValue(this.app.name)}. Do you want to replace it with an installed instance?`,
|
|
153
|
+
default: false,
|
|
154
|
+
});
|
|
155
|
+
if (!continueInstall) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
// uninstall the existing app
|
|
159
|
+
const spinner = ora();
|
|
160
|
+
spinner.start('Removing development app...');
|
|
161
|
+
try {
|
|
162
|
+
await this.api.put({
|
|
163
|
+
adminPath: `/client/apps/${existingSourcedApps.results[0].app_id}`,
|
|
164
|
+
}, {
|
|
165
|
+
body: { uninstalled: true },
|
|
166
|
+
});
|
|
167
|
+
spinner.succeed('Development app removed.');
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
spinner.fail(`Error removing development app from ${style.storeId(storeToInstall)}.`);
|
|
171
|
+
this.logError(err.message);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
2
|
+
export default class AppPush extends RemoteAppCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
file: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
7
|
+
};
|
|
8
|
+
static orientation: {
|
|
9
|
+
env: string;
|
|
10
|
+
};
|
|
11
|
+
static summary: string;
|
|
12
|
+
run(): Promise<void>;
|
|
13
|
+
private logDashboardUrl;
|
|
14
|
+
private pushConfigPath;
|
|
15
|
+
private pushFile;
|
|
16
|
+
private confirmRemoveInstalledApp;
|
|
17
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { appConfigFileExists } from '../../lib/app-config.js';
|
|
6
|
+
import { ConfigPaths, allConfigFilesPathsByType, appConfigFromFile, getConfigTypeFromPath, getConfigTypeKeyFromValue, } from '../../lib/apps/index.js';
|
|
7
|
+
import style from '../../lib/style.js';
|
|
8
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
9
|
+
import { default as swellConfig } from '../../lib/config.js';
|
|
10
|
+
export default class AppPush extends RemoteAppCommand {
|
|
11
|
+
static description = `Push all app configurations, a specific file, or a specific configuration
|
|
12
|
+
type to an app in your store's test environment. If the app does not exist yet,
|
|
13
|
+
it will be created and its global ID saved to a .swellrc file.
|
|
14
|
+
|
|
15
|
+
- If no file is specified, all configuration files will be pushed to the store.
|
|
16
|
+
This includes the app icon (assets/icon.png) and swell.json.
|
|
17
|
+
- If a file is specified, only that file will be pushed to the store.
|
|
18
|
+
- If a directory is specified, only files in that directory will be pushed.
|
|
19
|
+
|
|
20
|
+
App configuration directories:
|
|
21
|
+
${Object.values(ConfigPaths)
|
|
22
|
+
.map((dir) => style.path(`${dir}/`))
|
|
23
|
+
.join('\n')}`;
|
|
24
|
+
static examples = [
|
|
25
|
+
'swell app push',
|
|
26
|
+
'swell app push -f content',
|
|
27
|
+
'swell app push -f models/products.json',
|
|
28
|
+
];
|
|
29
|
+
static flags = {
|
|
30
|
+
file: Flags.string({
|
|
31
|
+
char: 'f',
|
|
32
|
+
description: 'relative path to a configuration file or directory to push',
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
static orientation = {
|
|
36
|
+
env: 'test',
|
|
37
|
+
};
|
|
38
|
+
static summary = 'Push configuration files to your Swell app.';
|
|
39
|
+
async run() {
|
|
40
|
+
const { flags } = await this.parse(AppPush);
|
|
41
|
+
const { file } = flags;
|
|
42
|
+
const currentStore = swellConfig.getDefaultStore();
|
|
43
|
+
this.api.setStoreEnv(currentStore, 'test');
|
|
44
|
+
this.app = await this.getAppWithConfig(undefined, false);
|
|
45
|
+
const confirmExistingApp = await this.confirmRemoveInstalledApp(currentStore);
|
|
46
|
+
if (confirmExistingApp === false) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// If app exists but owned by another client, offer to create a new instance
|
|
50
|
+
if (this.app.client_id !== currentStore) {
|
|
51
|
+
if (!confirmExistingApp?.id) {
|
|
52
|
+
const confirmPushNew = await confirm({
|
|
53
|
+
message: `${style.appConfigValue(this.app.name)} has a dev version in another store (${style.storeId(this.app.client_id)}). Do you want to create a new development instance in ${style.storeId(currentStore)}?`,
|
|
54
|
+
default: false,
|
|
55
|
+
});
|
|
56
|
+
if (!confirmPushNew) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// create a new app
|
|
61
|
+
this.app = await this.getCreateUpdateApp();
|
|
62
|
+
if (!this.app) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (!file) {
|
|
67
|
+
// update existing app if not targeting a file
|
|
68
|
+
this.app = await this.getCreateUpdateApp(this.app);
|
|
69
|
+
}
|
|
70
|
+
if (file) {
|
|
71
|
+
const filePath = path.join(this.appPath, file);
|
|
72
|
+
if (!appConfigFileExists(filePath)) {
|
|
73
|
+
this.error(`Path ${style.path(file)} does not exist`);
|
|
74
|
+
}
|
|
75
|
+
const filePathParsed = path.parse(file);
|
|
76
|
+
// if no directory is identified and no extension, it means the user
|
|
77
|
+
// passed in a config directory
|
|
78
|
+
if (!filePathParsed.dir && !filePathParsed.ext) {
|
|
79
|
+
await this.pushConfigPath(filePathParsed.base);
|
|
80
|
+
this.logDashboardUrl(flags, currentStore);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
await this.pushFile(filePath);
|
|
84
|
+
this.logDashboardUrl(flags, currentStore);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const syncResult = await this.syncAppConfigs();
|
|
88
|
+
const { installed, removed } = syncResult;
|
|
89
|
+
this.log(`Pushed ${installed.length} ${installed.length === 1 ? 'configuration' : 'configurations'}${removed.length > 0 ? ` (removed ${removed.length}).` : '.'}`);
|
|
90
|
+
this.logDashboardUrl(flags, currentStore);
|
|
91
|
+
}
|
|
92
|
+
logDashboardUrl(flags, storeId) {
|
|
93
|
+
this.log(`View the ${style.appConfigValue(this.app.name)} app in your dashboard at ${style.link(this.dashboardUrl(flags, storeId))}.`);
|
|
94
|
+
}
|
|
95
|
+
async pushConfigPath(path) {
|
|
96
|
+
const configType = getConfigTypeFromPath(path);
|
|
97
|
+
const configTypeKey = getConfigTypeKeyFromValue(configType);
|
|
98
|
+
if (!configTypeKey) {
|
|
99
|
+
this.error(`Invalid config path: ${path}`);
|
|
100
|
+
}
|
|
101
|
+
let pushedFiles = 0;
|
|
102
|
+
for (const { configFile } of allConfigFilesPathsByType(this.appPath, configTypeKey)) {
|
|
103
|
+
const appConfig = appConfigFromFile(configFile, configType);
|
|
104
|
+
// eslint-disable-next-line no-await-in-loop
|
|
105
|
+
await this.pushConfigFile(appConfig);
|
|
106
|
+
pushedFiles += 1;
|
|
107
|
+
}
|
|
108
|
+
if (pushedFiles === 0) {
|
|
109
|
+
this.log(`No valid files to push found for ${style.path(path)}.`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
this.log(`Pushed ${style.path(path)} to ${style.appConfigValue(this.app.name)}.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async pushFile(file) {
|
|
116
|
+
const configType = getConfigTypeFromPath(path.basename(path.dirname(file)));
|
|
117
|
+
if (!configType) {
|
|
118
|
+
this.error(`File ${file} is not a valid file to push.`);
|
|
119
|
+
}
|
|
120
|
+
const appConfig = appConfigFromFile(file, configType);
|
|
121
|
+
await this.pushConfigFile(appConfig);
|
|
122
|
+
}
|
|
123
|
+
async confirmRemoveInstalledApp(currentStore) {
|
|
124
|
+
if (!this.app.private_id) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
const existingInstalledApp = await this.api.get({
|
|
128
|
+
adminPath: `/client/apps/${this.app.private_id}`,
|
|
129
|
+
}, { query: { versioned: true } });
|
|
130
|
+
if (existingInstalledApp) {
|
|
131
|
+
const continuePush = await confirm({
|
|
132
|
+
message: `${style.storeId(currentStore)} has an installed version of ${style.appConfigValue(this.app.name)} v${existingInstalledApp.version}. Do you want to replace it with a development instance?`,
|
|
133
|
+
default: false,
|
|
134
|
+
});
|
|
135
|
+
if (!continuePush) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// uninstall the existing app
|
|
139
|
+
const spinner = ora();
|
|
140
|
+
spinner.start('Removing installed app...');
|
|
141
|
+
try {
|
|
142
|
+
await this.api.put({
|
|
143
|
+
adminPath: `/client/apps/${this.app.id}`,
|
|
144
|
+
}, {
|
|
145
|
+
body: { uninstalled: true },
|
|
146
|
+
});
|
|
147
|
+
spinner.succeed('Installed app removed.');
|
|
148
|
+
this.log();
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
spinner.fail(`Error removing installed app from ${style.storeId(currentStore)}.`);
|
|
152
|
+
this.logError(err.message);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return existingInstalledApp;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
2
|
+
export default class AppVersion extends RemoteAppCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
nextVersion: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
'git-tag': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
10
|
+
message: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
};
|
|
12
|
+
static summary: string;
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
private gitCommit;
|
|
15
|
+
private gitTag;
|
|
16
|
+
private updateRemote;
|
|
17
|
+
private validateAndComputeVersion;
|
|
18
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import git from '@npmcli/git';
|
|
3
|
+
import { Args, Flags } from '@oclif/core';
|
|
4
|
+
import { CLIError } from '@oclif/errors';
|
|
5
|
+
import * as semver from 'semver';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import style from '../../lib/style.js';
|
|
8
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
9
|
+
const RELEASE_TYPES = [
|
|
10
|
+
'major',
|
|
11
|
+
'premajor',
|
|
12
|
+
'minor',
|
|
13
|
+
'preminor',
|
|
14
|
+
'patch',
|
|
15
|
+
'prepatch',
|
|
16
|
+
'prerelease',
|
|
17
|
+
];
|
|
18
|
+
const TAG_VERSION_PREFIX = 'v';
|
|
19
|
+
export default class AppVersion extends RemoteAppCommand {
|
|
20
|
+
static args = {
|
|
21
|
+
nextVersion: Args.string({
|
|
22
|
+
description: `semver version or release type: ${[
|
|
23
|
+
'<next.version>',
|
|
24
|
+
...RELEASE_TYPES,
|
|
25
|
+
].join('|')}`,
|
|
26
|
+
async parse(input) {
|
|
27
|
+
if (RELEASE_TYPES.includes(input)) {
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
30
|
+
const parsedInput = semver.valid(input);
|
|
31
|
+
if (parsedInput) {
|
|
32
|
+
return parsedInput;
|
|
33
|
+
}
|
|
34
|
+
throw new CLIError(`Invalid version: ${input}`);
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
static description = `When executed without specifying NEXTVERSION, the command will display the current
|
|
39
|
+
version of your app.
|
|
40
|
+
|
|
41
|
+
If a NEXTVERSION is provided, the command will validate and compute the version
|
|
42
|
+
according to Semver rules by only allowing incremental updates.
|
|
43
|
+
|
|
44
|
+
The version change will be committed to the Git repository using the provided
|
|
45
|
+
message or a default similar to the tag, e.g. "v1.0.1".
|
|
46
|
+
|
|
47
|
+
By default, it creates a Git tag for the specified version. You can disable this
|
|
48
|
+
behavior by using the --git-tag=false option.`;
|
|
49
|
+
static examples = [
|
|
50
|
+
'swell app version',
|
|
51
|
+
'swell app version minor',
|
|
52
|
+
'swell app version 1.0.0 -m "First release"',
|
|
53
|
+
];
|
|
54
|
+
static flags = {
|
|
55
|
+
'git-tag': Flags.boolean({
|
|
56
|
+
default: true,
|
|
57
|
+
description: 'create a git tag for this version (default true)',
|
|
58
|
+
}),
|
|
59
|
+
message: Flags.string({
|
|
60
|
+
char: 'm',
|
|
61
|
+
description: 'description of the changes in this version',
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
static summary = 'Increment the version of your Swell app.';
|
|
65
|
+
async run() {
|
|
66
|
+
const { args, flags } = await this.parse(AppVersion);
|
|
67
|
+
const { 'git-tag': gitTag } = flags;
|
|
68
|
+
let { message } = flags;
|
|
69
|
+
let { nextVersion } = args;
|
|
70
|
+
// load app from remote
|
|
71
|
+
this.app = await this.getAppWithConfig();
|
|
72
|
+
const remoteVersion = this.app.version;
|
|
73
|
+
const versions = await this.getVersions();
|
|
74
|
+
// if the user didn't set a version, show the current one
|
|
75
|
+
if (!nextVersion) {
|
|
76
|
+
if (versions.length > 0) {
|
|
77
|
+
const lastVersion = versions[0].version;
|
|
78
|
+
this.log(`${style.appConfigValue(this.app.name)} latest version is ${style.appConfigValue(lastVersion)}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.log(`${style.appConfigValue(this.app.name)} has not been versioned yet`);
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
nextVersion = this.validateAndComputeVersion(nextVersion, remoteVersion);
|
|
86
|
+
if (!message) {
|
|
87
|
+
message = await input({
|
|
88
|
+
message: 'Describe the changes in this version',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const continueVersion = await confirm({
|
|
92
|
+
message: `Create ${style.appConfigValue(this.app.name)} version ${nextVersion}?`,
|
|
93
|
+
});
|
|
94
|
+
if (!continueVersion) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!message) {
|
|
98
|
+
message = `${TAG_VERSION_PREFIX}${nextVersion}`;
|
|
99
|
+
}
|
|
100
|
+
const spinner = ora();
|
|
101
|
+
spinner.start('Creating version...');
|
|
102
|
+
try {
|
|
103
|
+
await this.updateRemote(nextVersion, message, spinner);
|
|
104
|
+
await this.gitCommit(message);
|
|
105
|
+
if (gitTag)
|
|
106
|
+
await this.gitTag(nextVersion);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
spinner.fail(err.message);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
spinner.succeed(`${style.appConfigValue(this.app.name)} version ${nextVersion} created.`);
|
|
113
|
+
}
|
|
114
|
+
async gitCommit(message) {
|
|
115
|
+
// check if there are changes in the app config file
|
|
116
|
+
// if there are no changes, there is nothing to commit
|
|
117
|
+
const configStatus = await git.spawn(['status', this.appConfig.path], {
|
|
118
|
+
cwd: this.appConfig.cwd,
|
|
119
|
+
});
|
|
120
|
+
if (configStatus.stdout.includes('nothing to commit')) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// add the app config file to commit - this will contain the new version
|
|
124
|
+
await git.spawn(['add', this.appConfig.path]);
|
|
125
|
+
// commit the version change to git. It skips the git hooks with -n
|
|
126
|
+
await git.spawn(['commit', '-n', '-m', message]);
|
|
127
|
+
}
|
|
128
|
+
async gitTag(nextVersion) {
|
|
129
|
+
const tag = `${TAG_VERSION_PREFIX}${nextVersion}`;
|
|
130
|
+
// tag the version in git
|
|
131
|
+
await git.spawn(['tag', tag], { cwd: process.cwd() });
|
|
132
|
+
}
|
|
133
|
+
async updateRemote(nextVersion, message, spinner) {
|
|
134
|
+
// update the version on the remote
|
|
135
|
+
const versionResponse = await this.api.post({ adminPath: `/apps/${this.app.id}/versions` }, { body: { description: message, version: nextVersion } });
|
|
136
|
+
if (!versionResponse || !versionResponse.version) {
|
|
137
|
+
spinner.fail('Something went wrong.');
|
|
138
|
+
}
|
|
139
|
+
// update the local app config file since remote was successful
|
|
140
|
+
this.appConfig.set('version', nextVersion);
|
|
141
|
+
}
|
|
142
|
+
validateAndComputeVersion(nextVersion, remoteVersion) {
|
|
143
|
+
let version = nextVersion;
|
|
144
|
+
// compute the value of the next version and check if it's valid
|
|
145
|
+
if (RELEASE_TYPES.includes(nextVersion)) {
|
|
146
|
+
// increment the version
|
|
147
|
+
version = semver.inc(remoteVersion, version);
|
|
148
|
+
}
|
|
149
|
+
this.debug(`next version: ${version}`, `remote version: ${remoteVersion}`);
|
|
150
|
+
if (semver.eq(version, remoteVersion) ||
|
|
151
|
+
semver.lt(version, remoteVersion)) {
|
|
152
|
+
this.error(`The version you are trying to create ${version} needs to be greater than the current version ${remoteVersion}`);
|
|
153
|
+
}
|
|
154
|
+
return version;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
2
|
+
export default class AppWatch extends RemoteAppCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
'sync-all': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
7
|
+
};
|
|
8
|
+
static orientation: {
|
|
9
|
+
env: string;
|
|
10
|
+
};
|
|
11
|
+
static summary: string;
|
|
12
|
+
private watchListener;
|
|
13
|
+
run(): Promise<void>;
|
|
14
|
+
private handleFileChange;
|
|
15
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { appConfigFileExists } from '../../lib/app-config.js';
|
|
5
|
+
import { appConfigFromFile, findAppConfig, getConfigTypeFromPath, hasAllowedFileExtension, } from '../../lib/apps/index.js';
|
|
6
|
+
import style from '../../lib/style.js';
|
|
7
|
+
import { RemoteAppCommand } from '../../remote-app-command.js';
|
|
8
|
+
export default class AppWatch extends RemoteAppCommand {
|
|
9
|
+
static description = `Watches configuration files including functions, and automatically pushes
|
|
10
|
+
to the remote store.
|
|
11
|
+
|
|
12
|
+
Optionally synchronize all configs on change.`;
|
|
13
|
+
static examples = [
|
|
14
|
+
'swell app watch',
|
|
15
|
+
'swell app watch -a',
|
|
16
|
+
'swell app watch --sync-all',
|
|
17
|
+
];
|
|
18
|
+
static flags = {
|
|
19
|
+
'sync-all': Flags.boolean({
|
|
20
|
+
char: 'a',
|
|
21
|
+
default: false,
|
|
22
|
+
description: 'synchronize all app configurations on file change',
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
static orientation = {
|
|
26
|
+
env: 'test',
|
|
27
|
+
};
|
|
28
|
+
static summary = 'Watch for changes and push app configurations.';
|
|
29
|
+
watchListener = async (eventType, filename) => {
|
|
30
|
+
const configFile = filename;
|
|
31
|
+
const pathParsed = path.parse(configFile);
|
|
32
|
+
const fileDirs = pathParsed.dir.split(path.sep);
|
|
33
|
+
// only the first directory is relevant for us
|
|
34
|
+
const configDir = fileDirs[0];
|
|
35
|
+
// we only want to watch files in the root of the app directory
|
|
36
|
+
// and with the allowed extensions
|
|
37
|
+
if (!configDir ||
|
|
38
|
+
fileDirs.length > 1 ||
|
|
39
|
+
!hasAllowedFileExtension(configFile)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const configType = getConfigTypeFromPath(configDir);
|
|
43
|
+
if (!configType) {
|
|
44
|
+
this.debug(`watchListener: ${filename} is not a valid config type`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let watchFileEvent;
|
|
48
|
+
// per node docs:
|
|
49
|
+
//
|
|
50
|
+
// On most platforms, 'rename' is emitted whenever a filename appears
|
|
51
|
+
// or disappears in the directory.
|
|
52
|
+
//
|
|
53
|
+
// we want however to identify delete file events so we can send the
|
|
54
|
+
// correct API request
|
|
55
|
+
if (eventType === 'rename') {
|
|
56
|
+
watchFileEvent = 'new';
|
|
57
|
+
if (!appConfigFileExists(path.join(this.appPath, configFile))) {
|
|
58
|
+
watchFileEvent = 'deleted';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
watchFileEvent = 'change';
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await this.handleFileChange(configFile, configType, watchFileEvent);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// we don't want to break the watcher if an error is thrown while
|
|
69
|
+
// handling a file change
|
|
70
|
+
this.debug(error);
|
|
71
|
+
// for tests though, we want to break the watcher
|
|
72
|
+
if (process.env.NODE_ENV === 'test') {
|
|
73
|
+
// throwing does not break the test so we need to hard exit and log
|
|
74
|
+
// the error
|
|
75
|
+
console.error(style.error(error));
|
|
76
|
+
// eslint-disable-next-line no-process-exit, unicorn/no-process-exit
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// refetch app to get updated configs
|
|
81
|
+
this.app = await this.getAppWithConfig(this.app.id);
|
|
82
|
+
};
|
|
83
|
+
async run() {
|
|
84
|
+
const { flags } = await this.parse(AppWatch);
|
|
85
|
+
this.app = await this.getAppWithConfig();
|
|
86
|
+
this.log('Watching for changes...');
|
|
87
|
+
if (flags['sync-all']) {
|
|
88
|
+
await this.syncAppConfigs();
|
|
89
|
+
fs.watch(this.appPath, { recursive: true }, async (eventType, filename) => {
|
|
90
|
+
if (filename && hasAllowedFileExtension(filename)) {
|
|
91
|
+
await this.syncAppConfigs();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
fs.watch(this.appPath, { recursive: true }, this.watchListener);
|
|
97
|
+
}
|
|
98
|
+
async handleFileChange(configFile, configType, event) {
|
|
99
|
+
if (event === 'deleted') {
|
|
100
|
+
const appConfig = findAppConfig(this.app, configFile, configType);
|
|
101
|
+
if (!appConfig) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
return this.removeConfig(appConfig);
|
|
105
|
+
}
|
|
106
|
+
const appConfig = appConfigFromFile(path.join(this.appPath, configFile), configType);
|
|
107
|
+
const result = await this.pushConfigFile(appConfig);
|
|
108
|
+
this.debug(`handleFileChange: ${configFile} pushed`, result);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SwellCommand } from '../../swell-command.js';
|
|
2
|
+
export default class CreateApp extends SwellCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
id: import("@oclif/core/lib/interfaces/parser.js").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static flags: {
|
|
7
|
+
pkg: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
8
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
9
|
+
};
|
|
10
|
+
static description: string;
|
|
11
|
+
static examples: {
|
|
12
|
+
command: string;
|
|
13
|
+
description: string;
|
|
14
|
+
}[];
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
private tryPackageSetup;
|
|
17
|
+
private setupPackage;
|
|
18
|
+
private findPackageManager;
|
|
19
|
+
private doesPackageManagerExist;
|
|
20
|
+
private createAppConfigFolders;
|
|
21
|
+
}
|