@swell/cli 2.9.32 → 2.9.33
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/dist/commands/app/dev.js +2 -1
- package/dist/commands/app/frontend/dev.js +4 -2
- package/dist/commands/app/push.d.ts +1 -0
- package/dist/commands/app/push.js +38 -3
- package/dist/commands/app/version.js +3 -0
- package/dist/commands/create/function.js +14 -4
- package/dist/commands/create/model.js +9 -24
- package/dist/commands/inspect/components.d.ts +31 -0
- package/dist/commands/inspect/components.js +110 -0
- package/dist/commands/inspect/extensions.js +18 -9
- package/dist/commands/inspect/frontend.d.ts +37 -0
- package/dist/commands/inspect/frontend.js +75 -0
- package/dist/commands/inspect/functions.d.ts +11 -1
- package/dist/commands/inspect/functions.js +89 -6
- package/dist/commands/inspect/index.js +2 -0
- package/dist/inspect-resource-command.d.ts +1 -1
- package/dist/inspect-resource-command.js +8 -2
- package/dist/lib/api-errors.d.ts +1 -0
- package/dist/lib/api-errors.js +28 -0
- package/dist/lib/apps/components.d.ts +7 -0
- package/dist/lib/apps/components.js +10 -0
- package/dist/lib/apps/extension-manifest.d.ts +1 -0
- package/dist/lib/apps/extension-manifest.js +43 -0
- package/dist/lib/apps/index.d.ts +1 -0
- package/dist/lib/apps/index.js +30 -0
- package/dist/lib/apps/paths.d.ts +8 -0
- package/dist/lib/apps/paths.js +34 -4
- package/dist/lib/bundle.js +15 -2
- package/dist/lib/create/function.d.ts +1 -1
- package/dist/lib/create/function.js +9 -5
- package/dist/lib/create/index.js +2 -1
- package/dist/lib/create/model.d.ts +7 -3
- package/dist/lib/create/model.js +67 -9
- package/dist/lib/inspect/extensions.d.ts +4 -3
- package/dist/lib/inspect/extensions.js +36 -5
- package/dist/lib/swell-function-wrapper.js +5 -4
- package/dist/push-app-command.js +32 -19
- package/dist/remote-app-command.d.ts +1 -1
- package/dist/remote-app-command.js +78 -47
- package/oclif.manifest.json +139 -6
- package/package.json +1 -1
package/dist/commands/app/dev.js
CHANGED
|
@@ -105,7 +105,8 @@ export default class AppDev extends PushAppCommand {
|
|
|
105
105
|
this.jsonEvents = flags['json-events'];
|
|
106
106
|
this.setStoreUser(flags['store-user']);
|
|
107
107
|
this.functionFilter = flags.function ?? null;
|
|
108
|
-
|
|
108
|
+
// A pushing dev run updates the app from swell.json first, as a push does
|
|
109
|
+
if (!(await this.ensureAppExists(undefined, !noPush))) {
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
if (!noPush) {
|
|
@@ -42,10 +42,12 @@ export default class AppFrontendDev extends PushAppCommand {
|
|
|
42
42
|
const { port, 'proxy-port': proxyPort, 'app-dev': isAppDev } = flags;
|
|
43
43
|
const noPush = flags['no-push'];
|
|
44
44
|
this.setStoreUser(flags['store-user']);
|
|
45
|
-
|
|
45
|
+
const isPushing = !noPush && !isAppDev;
|
|
46
|
+
// A pushing dev run updates the app from swell.json first, as a push does
|
|
47
|
+
if (!(await this.ensureAppExists(undefined, isPushing))) {
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
|
-
if (
|
|
50
|
+
if (isPushing) {
|
|
49
51
|
await this.pushAppConfigs();
|
|
50
52
|
}
|
|
51
53
|
if (this.app.type === 'storefront') {
|
|
@@ -2,7 +2,8 @@ import { Args, Flags } from '@oclif/core';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { functionSlug, getContentActions, getSettingsActions, } from '../../lib/apps/actions.js';
|
|
5
|
-
import { affectsFrontendDeployment, AllConfigPaths, filePathExistsAsync, isPathDirectory, sameIntegrations, toPosixPath, } from '../../lib/apps/index.js';
|
|
5
|
+
import { affectsFrontendDeployment, AllConfigPaths, filePathExistsAsync, getNameFromFilePath, isPathDirectory, sameIntegrations, toPosixPath, } from '../../lib/apps/index.js';
|
|
6
|
+
import { isCredentialFile } from '../../lib/apps/paths.js';
|
|
6
7
|
import { default as localConfig } from '../../lib/config.js';
|
|
7
8
|
import style from '../../lib/style.js';
|
|
8
9
|
import { PushAppCommand } from '../../push-app-command.js';
|
|
@@ -149,11 +150,17 @@ ${Object.values(AllConfigPaths)
|
|
|
149
150
|
? this.pushFilePath(relativePath, force)
|
|
150
151
|
: this.pushFile(relativePath));
|
|
151
152
|
// Uploading frontend sources doesn't change what the Worker serves,
|
|
152
|
-
// so scoped pushes need the same pipeline as an unscoped one
|
|
153
|
-
|
|
153
|
+
// so scoped pushes need the same pipeline as an unscoped one. A
|
|
154
|
+
// credential file named on its own is refused and deploys nothing
|
|
155
|
+
if (!noDeploy &&
|
|
156
|
+
affectsFrontendDeployment(relativePath) &&
|
|
157
|
+
!isCredentialFile(relativePath)) {
|
|
154
158
|
await this.deployAppFrontend(force);
|
|
155
159
|
}
|
|
156
160
|
await this.checkActionFunctions(toPosixPath(relativePath));
|
|
161
|
+
if (toPosixPath(relativePath).split('/')[0] === 'functions') {
|
|
162
|
+
await this.enableAutoDisabledFunctions();
|
|
163
|
+
}
|
|
157
164
|
this.log();
|
|
158
165
|
this.logDashboardUrl(flags);
|
|
159
166
|
this.exitIfFilesFailed();
|
|
@@ -162,6 +169,7 @@ ${Object.values(AllConfigPaths)
|
|
|
162
169
|
}
|
|
163
170
|
await this.pushAppConfigs(force);
|
|
164
171
|
await this.checkActionFunctions();
|
|
172
|
+
await this.enableAutoDisabledFunctions();
|
|
165
173
|
if (!noDeploy) {
|
|
166
174
|
await this.deployAppFrontend(force);
|
|
167
175
|
this.log();
|
|
@@ -226,6 +234,33 @@ ${Object.values(AllConfigPaths)
|
|
|
226
234
|
}
|
|
227
235
|
}
|
|
228
236
|
}
|
|
237
|
+
// The platform switches off a function that keeps failing, and only an
|
|
238
|
+
// install of its config switches it on again, which a push skips for an
|
|
239
|
+
// unchanged file. Switching it on also resends the events it missed.
|
|
240
|
+
async enableAutoDisabledFunctions() {
|
|
241
|
+
try {
|
|
242
|
+
const { results } = await this.api.getAll({ adminPath: '/data/:functions' }, {
|
|
243
|
+
query: {
|
|
244
|
+
app_id: this.app.id,
|
|
245
|
+
auto_disabled: true,
|
|
246
|
+
fields: 'id, name',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
// A function whose push just failed stays off, as it would fail again
|
|
250
|
+
const failedNames = new Set(this.failedFiles
|
|
251
|
+
.filter((filePath) => path.posix.dirname(filePath) === AllConfigPaths.FUNCTION)
|
|
252
|
+
.map((filePath) => getNameFromFilePath(filePath)));
|
|
253
|
+
const disabled = (results || []).filter((fn) => !failedNames.has(fn.name));
|
|
254
|
+
if (disabled.length === 0) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
await Promise.all(disabled.map((fn) => this.api.put({ adminPath: `/data/:functions/${fn.id}` }, { body: { enabled: true } })));
|
|
258
|
+
ora().succeed(`Re-enabled ${disabled.length} auto-disabled function${disabled.length === 1 ? '' : 's'}: ${disabled.map((fn) => fn.name).join(', ')}`);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
ora().warn(`Skipped re-enabling auto-disabled functions: ${error.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
229
264
|
// A skipped file is a failed push, for scripts and CI too
|
|
230
265
|
exitIfFilesFailed() {
|
|
231
266
|
if (this.failedFiles.length > 0) {
|
|
@@ -121,6 +121,9 @@ behavior by using the --no-git-tag option.`;
|
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
123
123
|
nextVersion = await this.validateAndComputeVersion(nextVersion, localVersion);
|
|
124
|
+
// The app takes its details from swell.json before any upload, as a push
|
|
125
|
+
// does, so that details the app refuses fail first
|
|
126
|
+
this.app = await this.getCreateUpdateApp(this.app);
|
|
124
127
|
await this.pushAppConfigs();
|
|
125
128
|
await this.deployAppFrontend(false, false);
|
|
126
129
|
if (!message) {
|
|
@@ -4,7 +4,7 @@ import { CreateConfigCommand } from '../../create-config-command.js';
|
|
|
4
4
|
import { ConfigType } from '../../lib/apps/index.js';
|
|
5
5
|
import { getFunctionTemplate, } from '../../lib/create/function.js';
|
|
6
6
|
import { toFunctionFileName } from '../../lib/create/index.js';
|
|
7
|
-
const PERMITTED_TRIGGERS = ['cron', 'model', 'route'];
|
|
7
|
+
const PERMITTED_TRIGGERS = ['cron', 'model', 'route', 'action'];
|
|
8
8
|
const PERMITTED_MODES = [...PERMITTED_TRIGGERS, 'workflow'];
|
|
9
9
|
export default class CreateFunction extends CreateConfigCommand {
|
|
10
10
|
static args = {
|
|
@@ -14,7 +14,7 @@ export default class CreateFunction extends CreateConfigCommand {
|
|
|
14
14
|
}),
|
|
15
15
|
trigger: Args.string({
|
|
16
16
|
default: '',
|
|
17
|
-
description: 'cron | model | route | workflow',
|
|
17
|
+
description: 'cron | model | route | action | workflow',
|
|
18
18
|
options: PERMITTED_MODES,
|
|
19
19
|
}),
|
|
20
20
|
};
|
|
@@ -23,6 +23,7 @@ export default class CreateFunction extends CreateConfigCommand {
|
|
|
23
23
|
'$ swell create function order-handler.ts model -e order.created,order.updated -y',
|
|
24
24
|
'$ swell create function cleanup.ts cron -s "0 0 * * *" -y',
|
|
25
25
|
'$ swell create function api-endpoint.ts route -r public -y',
|
|
26
|
+
'$ swell create function mark-shipped.ts action -y',
|
|
26
27
|
'$ swell create function run-import.ts --type workflow -y',
|
|
27
28
|
'$ swell create function run-import.ts workflow -y',
|
|
28
29
|
];
|
|
@@ -46,6 +47,11 @@ export default class CreateFunction extends CreateConfigCommand {
|
|
|
46
47
|
flag: '-r, --route=<option>',
|
|
47
48
|
description: 'Access: public (default) | private',
|
|
48
49
|
},
|
|
50
|
+
{
|
|
51
|
+
name: 'action',
|
|
52
|
+
flag: '',
|
|
53
|
+
description: 'Runs from a dashboard action',
|
|
54
|
+
},
|
|
49
55
|
{
|
|
50
56
|
name: 'workflow',
|
|
51
57
|
flag: '--type=workflow',
|
|
@@ -104,7 +110,7 @@ export default class CreateFunction extends CreateConfigCommand {
|
|
|
104
110
|
}
|
|
105
111
|
// Validate trigger argument
|
|
106
112
|
if (!trigger) {
|
|
107
|
-
this.error('Missing required argument for non-interactive mode: TRIGGER or --type workflow\n\nValid modes: cron, model, route, workflow\n\nExample: swell create function my-func.ts model -e order.created -y\nExample: swell create function run-import.ts --type workflow -y', { exit: 1 });
|
|
113
|
+
this.error('Missing required argument for non-interactive mode: TRIGGER or --type workflow\n\nValid modes: cron, model, route, action, workflow\n\nExample: swell create function my-func.ts model -e order.created -y\nExample: swell create function run-import.ts --type workflow -y', { exit: 1 });
|
|
108
114
|
}
|
|
109
115
|
// Validate filename has extension
|
|
110
116
|
const language = args.name.endsWith('.ts')
|
|
@@ -212,6 +218,10 @@ export default class CreateFunction extends CreateConfigCommand {
|
|
|
212
218
|
name: 'HTTP (route)',
|
|
213
219
|
value: 'route',
|
|
214
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: 'Dashboard action (action)',
|
|
223
|
+
value: 'action',
|
|
224
|
+
},
|
|
215
225
|
],
|
|
216
226
|
message: 'How will your function be triggered?',
|
|
217
227
|
});
|
|
@@ -290,7 +300,7 @@ export default class CreateFunction extends CreateConfigCommand {
|
|
|
290
300
|
if (typeFlag === 'workflow') {
|
|
291
301
|
if (positionalTrigger &&
|
|
292
302
|
PERMITTED_TRIGGERS.includes(positionalTrigger)) {
|
|
293
|
-
this.error('--type workflow cannot be combined with a positional cron, model, or
|
|
303
|
+
this.error('--type workflow cannot be combined with a positional cron, model, route, or action trigger.', { exit: 1 });
|
|
294
304
|
}
|
|
295
305
|
return 'workflow';
|
|
296
306
|
}
|
|
@@ -3,7 +3,7 @@ import { Args, Flags } from '@oclif/core';
|
|
|
3
3
|
import { CreateCollectionCommand } from '../../create-collection-command.js';
|
|
4
4
|
import { ConfigType } from '../../lib/apps/index.js';
|
|
5
5
|
import { toCollectionLabel, toCollectionName, toFileName, } from '../../lib/create/index.js';
|
|
6
|
-
import { parseEvents, parseFields, } from '../../lib/create/model.js';
|
|
6
|
+
import { FIELD_TYPES, fieldPairProblem, parseEvents, parseFields, } from '../../lib/create/model.js';
|
|
7
7
|
import { SCHEMAS } from '../../lib/create/schemas.js';
|
|
8
8
|
export default class CreateModel extends CreateCollectionCommand {
|
|
9
9
|
static args = {
|
|
@@ -16,7 +16,8 @@ export default class CreateModel extends CreateCollectionCommand {
|
|
|
16
16
|
static examples = [
|
|
17
17
|
'$ swell create model',
|
|
18
18
|
'$ swell create model products -y',
|
|
19
|
-
'$ swell create model visitors -f title:string,photo:
|
|
19
|
+
'$ swell create model visitors -f title:string,photo:file -l "Visitors" -y',
|
|
20
|
+
'$ swell create model reviews -f owner:link:accounts,tags:array:string -y',
|
|
20
21
|
];
|
|
21
22
|
static helpMeta = {
|
|
22
23
|
usageDirect: '<collection> [...] -y',
|
|
@@ -35,7 +36,7 @@ export default class CreateModel extends CreateCollectionCommand {
|
|
|
35
36
|
fields: Flags.string({
|
|
36
37
|
char: 'f',
|
|
37
38
|
default: '',
|
|
38
|
-
description: 'Fields as id:type pairs (e.g., title:string,
|
|
39
|
+
description: 'Fields as id:type pairs, id:link:<model> or id:array:<type> (e.g., title:string,owner:link:accounts)',
|
|
39
40
|
}),
|
|
40
41
|
events: Flags.string({
|
|
41
42
|
char: 'e',
|
|
@@ -58,14 +59,6 @@ export default class CreateModel extends CreateCollectionCommand {
|
|
|
58
59
|
const confirmYes = Boolean(flags.yes);
|
|
59
60
|
// Helper validators
|
|
60
61
|
const validEventValues = ['created', 'updated', 'deleted'];
|
|
61
|
-
const validFieldTypes = [
|
|
62
|
-
'collection',
|
|
63
|
-
'currency',
|
|
64
|
-
'image',
|
|
65
|
-
'int',
|
|
66
|
-
'link',
|
|
67
|
-
'string',
|
|
68
|
-
];
|
|
69
62
|
const validateEvents = (values) => {
|
|
70
63
|
const invalid = values.filter((v) => !validEventValues.includes(v));
|
|
71
64
|
if (invalid.length > 0) {
|
|
@@ -73,19 +66,11 @@ export default class CreateModel extends CreateCollectionCommand {
|
|
|
73
66
|
}
|
|
74
67
|
};
|
|
75
68
|
const validateFields = (pairs) => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
const type = pair.split(':')[1]?.trim();
|
|
83
|
-
if (!validFieldTypes.includes(type)) {
|
|
84
|
-
invalid.push(pair);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (invalid.length > 0) {
|
|
88
|
-
this.error(`Invalid field(s): ${invalid.join(', ')}\n\nValid types: ${validFieldTypes.join(', ')}\n\nExample: swell create model visitors -f title:string,price:currency -y`, { exit: 1 });
|
|
69
|
+
const problems = pairs
|
|
70
|
+
.map((pair) => fieldPairProblem(pair))
|
|
71
|
+
.filter(Boolean);
|
|
72
|
+
if (problems.length > 0) {
|
|
73
|
+
this.error(`Invalid field(s):\n ${problems.join('\n ')}\n\nValid types: ${FIELD_TYPES.filter((type) => type !== 'collection').join(', ')}\n\nExample: swell create model visitors -f title:string,owner:link:accounts,tags:array:string -y`, { exit: 1 });
|
|
89
74
|
}
|
|
90
75
|
};
|
|
91
76
|
if (confirmYes) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { InspectResourceCommand, InspectResourceCommandParsed } from '../../inspect-resource-command.js';
|
|
2
|
+
import { InspectScope } from '../../lib/apps/inspect-scope.js';
|
|
3
|
+
import { ComponentRecord } from '../../lib/inspect/extensions.js';
|
|
4
|
+
interface InspectedComponent extends ComponentRecord {
|
|
5
|
+
app_id: string;
|
|
6
|
+
build_file?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export default class Components extends InspectResourceCommand {
|
|
9
|
+
static summary: string;
|
|
10
|
+
static description: string;
|
|
11
|
+
static args: {
|
|
12
|
+
identifier: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
13
|
+
};
|
|
14
|
+
static flags: {
|
|
15
|
+
app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
16
|
+
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
17
|
+
json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
18
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
19
|
+
};
|
|
20
|
+
static examples: string[];
|
|
21
|
+
protected resourceLabel: string;
|
|
22
|
+
protected resourceLabelSingular: string;
|
|
23
|
+
protected adminPath: string;
|
|
24
|
+
protected commandName: string;
|
|
25
|
+
run(): Promise<void>;
|
|
26
|
+
protected getListResults(scope: InspectScope): Promise<any[]>;
|
|
27
|
+
protected metaFor(record: InspectedComponent): string | undefined;
|
|
28
|
+
protected showDetail(identifier: string, scope: InspectScope, flags: InspectResourceCommandParsed['flags']): Promise<void>;
|
|
29
|
+
private getComponents;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { InspectResourceCommand, inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
|
|
2
|
+
import { classifyIdentifier, } from '../../lib/apps/inspect-scope.js';
|
|
3
|
+
import { HEX24 } from '../../lib/apps/object-id.js';
|
|
4
|
+
import { resolveAppId } from '../../lib/apps/resolve.js';
|
|
5
|
+
import { componentExtensionId, } from '../../lib/inspect/extensions.js';
|
|
6
|
+
const COMPONENT_FIELDS = 'id, name, file_path, values, build_file, version, date_created, date_updated';
|
|
7
|
+
export default class Components extends InspectResourceCommand {
|
|
8
|
+
static summary = 'Components with their targets.';
|
|
9
|
+
static description = `Lists the components of installed apps, grouped by app. Pass --app=<slug> or --app=. (current swell.json) to scope.
|
|
10
|
+
|
|
11
|
+
List output: paste-back key + the targets the component renders in and the extension it belongs to.
|
|
12
|
+
|
|
13
|
+
Pass an identifier to view a single component config as JSON.
|
|
14
|
+
|
|
15
|
+
Identifier forms:
|
|
16
|
+
app.<app>.<name> — full paste-back key (list column 1)
|
|
17
|
+
<name> — requires --app= scope
|
|
18
|
+
<24-char id> — any scope
|
|
19
|
+
`;
|
|
20
|
+
static args = { ...inspectResourceBaseArgs };
|
|
21
|
+
static flags = { ...inspectResourceBaseFlags };
|
|
22
|
+
static examples = [
|
|
23
|
+
'swell inspect components',
|
|
24
|
+
'swell inspect components --app=my-app',
|
|
25
|
+
'swell inspect components app.my-app.card',
|
|
26
|
+
'swell inspect components card --app=my-app',
|
|
27
|
+
'swell inspect components --live',
|
|
28
|
+
];
|
|
29
|
+
resourceLabel = 'Components';
|
|
30
|
+
resourceLabelSingular = 'component';
|
|
31
|
+
adminPath = '/apps/:id/configs';
|
|
32
|
+
commandName = 'components';
|
|
33
|
+
async run() {
|
|
34
|
+
const { args, flags } = await this.parse(Components);
|
|
35
|
+
await this.runInspect({ args, flags });
|
|
36
|
+
}
|
|
37
|
+
async getListResults(scope) {
|
|
38
|
+
return this.getComponents(scope.appId);
|
|
39
|
+
}
|
|
40
|
+
metaFor(record) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
const targets = record.values?.targets;
|
|
43
|
+
const targetIds = Array.isArray(targets)
|
|
44
|
+
? targets
|
|
45
|
+
.map((target) => target?.id)
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
: [];
|
|
48
|
+
if (targetIds.length > 0) {
|
|
49
|
+
parts.push(targetIds.join(','));
|
|
50
|
+
}
|
|
51
|
+
const extension = componentExtensionId(record.values);
|
|
52
|
+
if (extension) {
|
|
53
|
+
parts.push(`extension ${extension}`);
|
|
54
|
+
}
|
|
55
|
+
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
56
|
+
}
|
|
57
|
+
async showDetail(identifier, scope, flags) {
|
|
58
|
+
const kind = classifyIdentifier(identifier);
|
|
59
|
+
if (kind.kind === 'invalid') {
|
|
60
|
+
this.error(`Invalid component identifier '${identifier}'. ` +
|
|
61
|
+
`Expected bare name, full slug (app.<app>.<name>), or 24-char id.`, { exit: 1 });
|
|
62
|
+
}
|
|
63
|
+
if (kind.kind === 'name' && !scope.appId) {
|
|
64
|
+
this.error(`Bare component name '${identifier}' requires --app=<slug> or --app=. to scope. ` +
|
|
65
|
+
`Alternatively, pass a full slug (app.<app>.<name>) or 24-char id.`, { exit: 1 });
|
|
66
|
+
}
|
|
67
|
+
let { appId } = scope;
|
|
68
|
+
if (kind.kind === 'slug') {
|
|
69
|
+
appId = HEX24.test(kind.appPart)
|
|
70
|
+
? kind.appPart
|
|
71
|
+
: await resolveAppId(this.api, kind.appPart);
|
|
72
|
+
}
|
|
73
|
+
const components = await this.getComponents(appId);
|
|
74
|
+
const record = components.find((component) => kind.kind === 'id'
|
|
75
|
+
? component.id === kind.id
|
|
76
|
+
: component.name === (kind.kind === 'slug' ? kind.name : identifier));
|
|
77
|
+
if (!record) {
|
|
78
|
+
this.throwNotFound(identifier);
|
|
79
|
+
}
|
|
80
|
+
this.emitDetail(record, flags);
|
|
81
|
+
}
|
|
82
|
+
// Components are configs of the app at its installed version; a
|
|
83
|
+
// development install has no version
|
|
84
|
+
async getComponents(appId) {
|
|
85
|
+
const installed = await this.api.get({ adminPath: '/client/apps' });
|
|
86
|
+
const apps = (installed?.results ?? []).filter((app) => !appId || app.app_id === appId);
|
|
87
|
+
const lists = await Promise.all(apps.map(async (app) => {
|
|
88
|
+
// An app whose configs the store cannot read is left out
|
|
89
|
+
let results;
|
|
90
|
+
try {
|
|
91
|
+
({ results } = await this.api.getAll({ adminPath: `/apps/${app.app_id}/configs` }, {
|
|
92
|
+
query: {
|
|
93
|
+
type: 'component',
|
|
94
|
+
fields: COMPONENT_FIELDS,
|
|
95
|
+
...(app.version ? { version: app.version } : undefined),
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
this.warn(`Components of ${app.app_public_id ?? app.app_id} could not be read: ${error.message}`);
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
// Shared source under components/ is pushed without a build
|
|
104
|
+
return (results ?? [])
|
|
105
|
+
.filter((component) => component.build_file)
|
|
106
|
+
.map((component) => ({ ...component, app_id: app.app_id }));
|
|
107
|
+
}));
|
|
108
|
+
return lists.flat();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -75,11 +75,12 @@ Identifier forms:
|
|
|
75
75
|
if (scope.appId) {
|
|
76
76
|
fnQuery.app_id = scope.appId;
|
|
77
77
|
}
|
|
78
|
+
const unavailableSettings = {};
|
|
78
79
|
const [installedAppsResp, payments, shipments, taxes, functionsResp] = await Promise.all([
|
|
79
80
|
this.api.get({ adminPath: '/client/apps' }),
|
|
80
|
-
this.fetchSafe('
|
|
81
|
-
this.fetchSafe('
|
|
82
|
-
this.fetchSafe('
|
|
81
|
+
this.fetchSafe('payments', unavailableSettings),
|
|
82
|
+
this.fetchSafe('shipments', unavailableSettings),
|
|
83
|
+
this.fetchSafe('taxes', unavailableSettings),
|
|
83
84
|
this.api.getAll({ adminPath: '/data/:functions' }, { query: fnQuery }),
|
|
84
85
|
]);
|
|
85
86
|
const installed = (installedAppsResp?.results ??
|
|
@@ -209,6 +210,7 @@ Identifier forms:
|
|
|
209
210
|
payments,
|
|
210
211
|
shipments,
|
|
211
212
|
taxes,
|
|
213
|
+
unavailableSettings,
|
|
212
214
|
functions: state.functions,
|
|
213
215
|
components: state.components,
|
|
214
216
|
});
|
|
@@ -276,11 +278,12 @@ Identifier forms:
|
|
|
276
278
|
`Expected bare id or app.<slug>.<extId>.`, { exit: 1 });
|
|
277
279
|
}
|
|
278
280
|
}
|
|
281
|
+
const unavailableSettings = {};
|
|
279
282
|
const [installedAppsResp, payments, shipments, taxes, functionsResp] = await Promise.all([
|
|
280
283
|
this.api.get({ adminPath: '/client/apps' }, { query: {} }),
|
|
281
|
-
this.fetchSafe('
|
|
282
|
-
this.fetchSafe('
|
|
283
|
-
this.fetchSafe('
|
|
284
|
+
this.fetchSafe('payments', unavailableSettings),
|
|
285
|
+
this.fetchSafe('shipments', unavailableSettings),
|
|
286
|
+
this.fetchSafe('taxes', unavailableSettings),
|
|
284
287
|
this.api.getAll({ adminPath: '/data/:functions' }, { query: { app_id: appId, extension: extId } }),
|
|
285
288
|
]);
|
|
286
289
|
const installed = (installedAppsResp?.results ??
|
|
@@ -321,6 +324,7 @@ Identifier forms:
|
|
|
321
324
|
payments,
|
|
322
325
|
shipments,
|
|
323
326
|
taxes,
|
|
327
|
+
unavailableSettings,
|
|
324
328
|
functions: fnsForExtension,
|
|
325
329
|
components: compsForExtension,
|
|
326
330
|
localDiff,
|
|
@@ -393,12 +397,17 @@ Identifier forms:
|
|
|
393
397
|
}
|
|
394
398
|
this.log();
|
|
395
399
|
}
|
|
396
|
-
|
|
400
|
+
// A settings record that cannot be read is noted, so that its extensions
|
|
401
|
+
// show the reason rather than look not activated
|
|
402
|
+
async fetchSafe(record, unavailableSettings) {
|
|
397
403
|
try {
|
|
398
|
-
const result = await this.api.get({
|
|
404
|
+
const result = await this.api.get({
|
|
405
|
+
adminPath: `/data/settings/${record}`,
|
|
406
|
+
});
|
|
399
407
|
return (result ?? null);
|
|
400
408
|
}
|
|
401
|
-
catch {
|
|
409
|
+
catch (error) {
|
|
410
|
+
unavailableSettings[record] = error.message;
|
|
402
411
|
return null;
|
|
403
412
|
}
|
|
404
413
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { InspectResourceCommand, InspectResourceCommandParsed } from '../../inspect-resource-command.js';
|
|
2
|
+
import { InspectScope } from '../../lib/apps/inspect-scope.js';
|
|
3
|
+
interface InspectedFrontend {
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
address: string;
|
|
6
|
+
app_id: string;
|
|
7
|
+
framework?: string;
|
|
8
|
+
hosting?: string;
|
|
9
|
+
installation_id: string;
|
|
10
|
+
}
|
|
11
|
+
export default class Frontend extends InspectResourceCommand {
|
|
12
|
+
static summary: string;
|
|
13
|
+
static description: string;
|
|
14
|
+
static args: {
|
|
15
|
+
identifier: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
16
|
+
};
|
|
17
|
+
static flags: {
|
|
18
|
+
app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
19
|
+
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
20
|
+
json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
21
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
22
|
+
};
|
|
23
|
+
static examples: string[];
|
|
24
|
+
protected resourceLabel: string;
|
|
25
|
+
protected resourceLabelSingular: string;
|
|
26
|
+
protected adminPath: string;
|
|
27
|
+
protected commandName: string;
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
protected getListResults(scope: InspectScope): Promise<any[]>;
|
|
30
|
+
protected keyFor(record: InspectedFrontend, appSlugById: Record<string, string>): string;
|
|
31
|
+
protected metaFor(record: InspectedFrontend): string | undefined;
|
|
32
|
+
protected showDetail(identifier: string, _scope: InspectScope, flags: InspectResourceCommandParsed['flags']): Promise<void>;
|
|
33
|
+
private getFrontends;
|
|
34
|
+
private getInstalledApps;
|
|
35
|
+
private frontendsOf;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { InspectResourceCommand, inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
|
|
2
|
+
import { slugFromInstalledApp } from '../../lib/apps/slug.js';
|
|
3
|
+
import { default as localConfig } from '../../lib/config.js';
|
|
4
|
+
import { getAppFrontendHost } from '../../lib/constants.js';
|
|
5
|
+
export default class Frontend extends InspectResourceCommand {
|
|
6
|
+
static summary = 'App frontends with hosting and address.';
|
|
7
|
+
static description = `Lists the frontends of installed apps. Pass --app=<slug> or --app=. (current swell.json) to scope.
|
|
8
|
+
|
|
9
|
+
List output: paste-back key + hosting (managed or self-hosted), framework, and the address the store serves the frontend at.
|
|
10
|
+
|
|
11
|
+
Pass an identifier to view an app's frontend as JSON: the app's frontend selection (package or self-hosted URL) with its installation and address.
|
|
12
|
+
|
|
13
|
+
Identifier forms:
|
|
14
|
+
app.<app> — full paste-back key (list column 1)
|
|
15
|
+
<app> — the app's slug
|
|
16
|
+
<24-char id> — the app's id
|
|
17
|
+
`;
|
|
18
|
+
static args = { ...inspectResourceBaseArgs };
|
|
19
|
+
static flags = { ...inspectResourceBaseFlags };
|
|
20
|
+
static examples = [
|
|
21
|
+
'swell inspect frontend',
|
|
22
|
+
'swell inspect frontend --app=.',
|
|
23
|
+
'swell inspect frontend app.my-app',
|
|
24
|
+
'swell inspect frontend --live',
|
|
25
|
+
];
|
|
26
|
+
resourceLabel = 'Frontends';
|
|
27
|
+
resourceLabelSingular = 'frontend';
|
|
28
|
+
adminPath = '/client/apps';
|
|
29
|
+
commandName = 'frontend';
|
|
30
|
+
async run() {
|
|
31
|
+
const { args, flags } = await this.parse(Frontend);
|
|
32
|
+
await this.runInspect({ args, flags });
|
|
33
|
+
}
|
|
34
|
+
async getListResults(scope) {
|
|
35
|
+
return this.getFrontends(scope.appId);
|
|
36
|
+
}
|
|
37
|
+
keyFor(record, appSlugById) {
|
|
38
|
+
return `app.${appSlugById[record.app_id] ?? record.app_id}`;
|
|
39
|
+
}
|
|
40
|
+
metaFor(record) {
|
|
41
|
+
// A frontend without hosting is self-hosted
|
|
42
|
+
return [record.hosting || 'self-hosted', record.framework, record.address]
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.join(' · ');
|
|
45
|
+
}
|
|
46
|
+
async showDetail(identifier, _scope, flags) {
|
|
47
|
+
const app = identifier.replace(/^app\./, '');
|
|
48
|
+
const installed = await this.getInstalledApps();
|
|
49
|
+
const match = installed.find((record) => record.app_id === app || slugFromInstalledApp(record) === app);
|
|
50
|
+
const [frontend] = match ? this.frontendsOf([match]) : [];
|
|
51
|
+
if (!frontend) {
|
|
52
|
+
this.throwNotFound(identifier);
|
|
53
|
+
}
|
|
54
|
+
this.emitDetail(frontend, flags);
|
|
55
|
+
}
|
|
56
|
+
async getFrontends(appId) {
|
|
57
|
+
const installed = await this.getInstalledApps();
|
|
58
|
+
return this.frontendsOf(installed.filter((record) => !appId || record.app_id === appId));
|
|
59
|
+
}
|
|
60
|
+
async getInstalledApps() {
|
|
61
|
+
const response = await this.api.get({ adminPath: '/client/apps' });
|
|
62
|
+
return (response?.results ?? []);
|
|
63
|
+
}
|
|
64
|
+
frontendsOf(installed) {
|
|
65
|
+
const store = localConfig.getDefaultStore();
|
|
66
|
+
return installed
|
|
67
|
+
.filter((record) => Object.keys(record.app?.frontend ?? {}).length > 0)
|
|
68
|
+
.map((record) => ({
|
|
69
|
+
...record.app?.frontend,
|
|
70
|
+
address: getAppFrontendHost(store, record.id),
|
|
71
|
+
app_id: record.app_id,
|
|
72
|
+
installation_id: record.id,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { InspectResourceCommand, InspectResourceCommandParsed } from '../../inspect-resource-command.js';
|
|
2
|
+
import { InspectScope } from '../../lib/apps/inspect-scope.js';
|
|
2
3
|
interface FunctionRecord {
|
|
3
4
|
id?: string;
|
|
4
5
|
kind?: string;
|
|
5
6
|
name?: string;
|
|
6
7
|
enabled?: boolean;
|
|
8
|
+
auto_disabled?: boolean;
|
|
9
|
+
attempts_failed?: number;
|
|
10
|
+
date_first_failed?: string;
|
|
7
11
|
cron?: {
|
|
8
12
|
schedule?: string;
|
|
9
13
|
};
|
|
@@ -18,6 +22,7 @@ interface FunctionRecord {
|
|
|
18
22
|
};
|
|
19
23
|
date_cron_scheduled?: string;
|
|
20
24
|
date_final_attempt?: string;
|
|
25
|
+
timeout?: number;
|
|
21
26
|
app_id?: string;
|
|
22
27
|
}
|
|
23
28
|
export default class Functions extends InspectResourceCommand {
|
|
@@ -37,10 +42,15 @@ export default class Functions extends InspectResourceCommand {
|
|
|
37
42
|
protected resourceLabelSingular: string;
|
|
38
43
|
protected adminPath: string;
|
|
39
44
|
protected commandName: string;
|
|
45
|
+
private inactiveApps;
|
|
40
46
|
run(): Promise<void>;
|
|
47
|
+
protected getListResults(scope: InspectScope, flags: InspectResourceCommandParsed['flags']): Promise<any[]>;
|
|
41
48
|
protected metaFor(r: FunctionRecord): string | undefined;
|
|
42
49
|
protected filterListResults<T>(results: T[]): T[];
|
|
43
|
-
protected emitDetail(record: FunctionRecord, flags: InspectResourceCommandParsed['flags']): void
|
|
50
|
+
protected emitDetail(record: FunctionRecord, flags: InspectResourceCommandParsed['flags']): Promise<void>;
|
|
51
|
+
private detailNotes;
|
|
52
|
+
private lastFailedDelivery;
|
|
53
|
+
private getInactiveApps;
|
|
44
54
|
/**
|
|
45
55
|
* Model-triggered functions emit `/events:webhooks` rows on each invocation.
|
|
46
56
|
* Route and cron functions do not — pointing the agent at an empty query
|