@swell/cli 2.9.31 → 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/action.d.ts +25 -0
- package/dist/commands/app/action.js +233 -0
- package/dist/commands/app/dev.js +2 -1
- package/dist/commands/app/frontend/deploy.d.ts +1 -0
- package/dist/commands/app/frontend/deploy.js +4 -0
- package/dist/commands/app/frontend/dev.js +4 -2
- package/dist/commands/app/push.d.ts +3 -0
- package/dist/commands/app/push.js +100 -4
- 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/create/webhook.js +7 -13
- 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/commands/schema.js +5 -0
- package/dist/help/custom-help.js +5 -1
- 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/actions.d.ts +45 -0
- package/dist/lib/apps/actions.js +114 -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/listing.d.ts +1 -0
- package/dist/lib/apps/listing.js +41 -0
- package/dist/lib/apps/paths.d.ts +8 -0
- package/dist/lib/apps/paths.js +34 -4
- package/dist/lib/bundle.js +26 -41
- 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/function-declarations.d.ts +8 -0
- package/dist/lib/function-declarations.js +27 -0
- package/dist/lib/inspect/extensions.d.ts +4 -3
- package/dist/lib/inspect/extensions.js +36 -5
- package/dist/lib/swell-function-wrapper.js +13 -5
- package/dist/lib/swell-workflow-request.d.ts +34 -0
- package/dist/lib/swell-workflow-request.js +58 -0
- package/dist/push-app-command.d.ts +2 -1
- package/dist/push-app-command.js +43 -22
- package/dist/remote-app-command.d.ts +2 -2
- package/dist/remote-app-command.js +79 -51
- package/oclif.manifest.json +263 -10
- package/package.json +2 -1
|
@@ -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
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InspectResourceCommand, inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
|
|
2
|
+
import { slugFromInstalledApp } from '../../lib/apps/slug.js';
|
|
2
3
|
function deriveTrigger(r) {
|
|
3
4
|
if (r.cron?.schedule) {
|
|
4
5
|
return `cron (${r.cron.schedule})`;
|
|
@@ -25,7 +26,7 @@ export default class Functions extends InspectResourceCommand {
|
|
|
25
26
|
static summary = 'Functions with scheduling and trigger state.';
|
|
26
27
|
static description = `Lists functions across all apps, grouped by app. Pass --app=<slug> or --app=. (current swell.json) to scope.
|
|
27
28
|
|
|
28
|
-
List output: paste-back key + status (disabled, trigger, next cron run, last
|
|
29
|
+
List output: paste-back key + status (disabled or auto-disabled, failing since and failure count, the date it gets disabled, trigger, next cron run) and whether its app is inactive, which stops all of its functions. The detail view also shows the last failed delivery.
|
|
29
30
|
|
|
30
31
|
Pass an identifier to view a single record as JSON.
|
|
31
32
|
|
|
@@ -47,15 +48,37 @@ Identifier forms:
|
|
|
47
48
|
resourceLabelSingular = 'function';
|
|
48
49
|
adminPath = '/data/:functions';
|
|
49
50
|
commandName = 'functions';
|
|
51
|
+
inactiveApps = [];
|
|
50
52
|
async run() {
|
|
51
53
|
const { args, flags } = await this.parse(Functions);
|
|
52
54
|
await this.runInspect({ args, flags });
|
|
53
55
|
}
|
|
56
|
+
async getListResults(scope, flags) {
|
|
57
|
+
const [results, inactiveApps] = await Promise.all([
|
|
58
|
+
super.getListResults(scope, flags),
|
|
59
|
+
this.getInactiveApps(),
|
|
60
|
+
]);
|
|
61
|
+
this.inactiveApps = inactiveApps;
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
54
64
|
metaFor(r) {
|
|
55
65
|
const parts = [];
|
|
56
|
-
if (
|
|
66
|
+
if (this.inactiveApps.some((app) => app.app_id === r.app_id)) {
|
|
67
|
+
parts.push('app inactive');
|
|
68
|
+
}
|
|
69
|
+
if (r.auto_disabled) {
|
|
70
|
+
parts.push('auto-disabled');
|
|
71
|
+
}
|
|
72
|
+
else if (!r.enabled) {
|
|
57
73
|
parts.push('disabled');
|
|
58
74
|
}
|
|
75
|
+
const failed = r.attempts_failed ?? 0;
|
|
76
|
+
if (failed > 0) {
|
|
77
|
+
const since = r.date_first_failed
|
|
78
|
+
? `failing since ${r.date_first_failed} · `
|
|
79
|
+
: '';
|
|
80
|
+
parts.push(`${since}${failed} fail${failed === 1 ? '' : 's'}`);
|
|
81
|
+
}
|
|
59
82
|
const trigger = deriveTrigger(r);
|
|
60
83
|
if (trigger) {
|
|
61
84
|
parts.push(trigger);
|
|
@@ -63,21 +86,60 @@ Identifier forms:
|
|
|
63
86
|
if (r.date_cron_scheduled) {
|
|
64
87
|
parts.push(`next ${r.date_cron_scheduled}`);
|
|
65
88
|
}
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
const disableDate = failed > 0 && !r.auto_disabled && getDisableDate(r);
|
|
90
|
+
if (disableDate) {
|
|
91
|
+
parts.push(`may disable from ${disableDate}`);
|
|
68
92
|
}
|
|
69
93
|
return parts.length > 0 ? parts.join(' · ') : undefined;
|
|
70
94
|
}
|
|
71
95
|
filterListResults(results) {
|
|
72
96
|
return results.filter((record) => record.kind !== 'workflow');
|
|
73
97
|
}
|
|
74
|
-
emitDetail(record, flags) {
|
|
98
|
+
async emitDetail(record, flags) {
|
|
75
99
|
if (record.kind === 'workflow') {
|
|
76
100
|
const workflowName = record.name || record.id;
|
|
77
101
|
const workflowRef = record.id || record.name;
|
|
78
102
|
throw new Error(`Function '${workflowName}' is a workflow. Use "swell inspect workflows ${workflowRef}" instead.`);
|
|
79
103
|
}
|
|
80
|
-
super.emitDetail(record, flags);
|
|
104
|
+
super.emitDetail(record, flags, flags.json ? [] : await this.detailNotes(record));
|
|
105
|
+
}
|
|
106
|
+
// Notes are best effort: a function shows without those it cannot read
|
|
107
|
+
async detailNotes(record) {
|
|
108
|
+
const notes = [];
|
|
109
|
+
const inactiveApps = await this.getInactiveApps().catch(() => []);
|
|
110
|
+
const inactiveApp = inactiveApps.find((app) => app.app_id === record.app_id);
|
|
111
|
+
if (inactiveApp) {
|
|
112
|
+
notes.push(`App ${slugFromInstalledApp(inactiveApp) ?? inactiveApp.app_id} is inactive: none of its functions run.`);
|
|
113
|
+
}
|
|
114
|
+
if (record.id &&
|
|
115
|
+
(record.auto_disabled || (record.attempts_failed ?? 0) > 0)) {
|
|
116
|
+
const failure = await this.lastFailedDelivery(record.id).catch(() => { });
|
|
117
|
+
if (failure) {
|
|
118
|
+
notes.push(`Last failed delivery: ${failure}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return notes;
|
|
122
|
+
}
|
|
123
|
+
async lastFailedDelivery(functionId) {
|
|
124
|
+
const response = await this.api.get({ adminPath: '/data/events:webhooks' }, { query: { function_id: functionId, sort: 'date_sent desc', limit: 1 } });
|
|
125
|
+
const delivery = response?.results?.[0];
|
|
126
|
+
if (!delivery) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const { date_sent: dateSent, response: answer, status } = delivery;
|
|
130
|
+
if (status >= 200 && status < 300) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const message = answer?.error?.message ??
|
|
134
|
+
answer?.message ??
|
|
135
|
+
(typeof answer === 'string' ? answer : undefined);
|
|
136
|
+
return [dateSent, status ?? 'no response', message]
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join(' · ');
|
|
139
|
+
}
|
|
140
|
+
async getInactiveApps() {
|
|
141
|
+
const response = await this.api.get({ adminPath: '/client/apps' });
|
|
142
|
+
return (response?.results ?? []).filter((app) => app.active === false);
|
|
81
143
|
}
|
|
82
144
|
/**
|
|
83
145
|
* Model-triggered functions emit `/events:webhooks` rows on each invocation.
|
|
@@ -96,3 +158,24 @@ Identifier forms:
|
|
|
96
158
|
return lines;
|
|
97
159
|
}
|
|
98
160
|
}
|
|
161
|
+
// A function with a timeout over this is switched off sooner
|
|
162
|
+
const LONG_TIMEOUT_MS = 10000;
|
|
163
|
+
const LONG_TIMEOUT_DISABLE_DAYS = 2;
|
|
164
|
+
/**
|
|
165
|
+
* When a failing function is switched off if it keeps failing: at its final
|
|
166
|
+
* attempt, or two days after its first failure when its timeout is long. A
|
|
167
|
+
* 404 switches it off at once.
|
|
168
|
+
* @param r The function
|
|
169
|
+
* @returns The date, as the API gives dates
|
|
170
|
+
*/
|
|
171
|
+
function getDisableDate(r) {
|
|
172
|
+
if ((r.timeout ?? 0) > LONG_TIMEOUT_MS) {
|
|
173
|
+
if (!r.date_first_failed) {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
const date = new Date(r.date_first_failed);
|
|
177
|
+
date.setUTCDate(date.getUTCDate() + LONG_TIMEOUT_DISABLE_DAYS);
|
|
178
|
+
return date.toISOString();
|
|
179
|
+
}
|
|
180
|
+
return r.date_final_attempt;
|
|
181
|
+
}
|
|
@@ -11,6 +11,8 @@ export default class Inspect extends Command {
|
|
|
11
11
|
'swell inspect workflow-runs --workflow app.my-app.run-import --status active',
|
|
12
12
|
'swell inspect notifications com.orders.receipt.v2',
|
|
13
13
|
'swell inspect settings app.my-app',
|
|
14
|
+
'swell inspect components --app=.',
|
|
15
|
+
'swell inspect frontend app.my-app',
|
|
14
16
|
];
|
|
15
17
|
async run() {
|
|
16
18
|
this.log('Run "swell inspect --help" to see available subcommands.');
|
package/dist/commands/schema.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as fs from 'node:fs/promises';
|
|
|
6
6
|
import parseJson from 'parse-json';
|
|
7
7
|
import { bundleFunction } from '../lib/bundle.js';
|
|
8
8
|
import { SCHEMAS_BASE_URL } from '../lib/constants.js';
|
|
9
|
+
import { getFunctionDeclarations } from '../lib/function-declarations.js';
|
|
9
10
|
import { analyzeFunctionSource, getFunctionTriggers, hasKindDiagnostic, workflowStaticConfigError, } from '../lib/function-source-analysis.js';
|
|
10
11
|
import { SwellCommand } from '../swell-command.js';
|
|
11
12
|
const SCHEMA_DEFINITIONS = Object.freeze({
|
|
@@ -164,6 +165,10 @@ export default class Schema extends SwellCommand {
|
|
|
164
165
|
break;
|
|
165
166
|
}
|
|
166
167
|
case 'dts': {
|
|
168
|
+
if (type === 'function') {
|
|
169
|
+
output = await getFunctionDeclarations(process.cwd());
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
167
172
|
const dtsUrl = this.getTypeScriptDeclarationsUrl(type);
|
|
168
173
|
const response = await fetch(dtsUrl);
|
|
169
174
|
if (!response.ok) {
|
package/dist/help/custom-help.js
CHANGED
|
@@ -280,7 +280,11 @@ export default class CustomHelp extends Help {
|
|
|
280
280
|
const value = ('helpValue' in flag && flag.helpValue) ||
|
|
281
281
|
('options' in flag && flag.options ? 'option' : 'value');
|
|
282
282
|
const suffix = flag.type === 'boolean' ? '' : `=<${value}>`;
|
|
283
|
-
|
|
283
|
+
const negatable = flag.type === 'boolean' && flag.allowNo;
|
|
284
|
+
return [
|
|
285
|
+
name,
|
|
286
|
+
`${shortChar}--${negatable ? '[no-]' : ''}${name}${suffix}`,
|
|
287
|
+
];
|
|
284
288
|
}));
|
|
285
289
|
const flagWidth = Math.max(30, ...[...flagLabels.values()].map((label) => label.length));
|
|
286
290
|
for (const [name, flag] of flagEntries) {
|
|
@@ -87,7 +87,7 @@ export declare abstract class InspectResourceCommand extends SwellCommand {
|
|
|
87
87
|
* `--json` is the machine-readable contract — hints belong in the same
|
|
88
88
|
* stream as the human-facing output and are simply omitted there.
|
|
89
89
|
*/
|
|
90
|
-
protected emitDetail(record: any, flags: InspectResourceCommandParsed['flags']): void;
|
|
90
|
+
protected emitDetail(record: any, flags: InspectResourceCommandParsed['flags'], notes?: string[]): Promise<void> | void;
|
|
91
91
|
private lookupByAppAndName;
|
|
92
92
|
private printPreamble;
|
|
93
93
|
protected throwNotFound(identifier: string): never;
|
|
@@ -189,7 +189,7 @@ export class InspectResourceCommand extends SwellCommand {
|
|
|
189
189
|
if (!record) {
|
|
190
190
|
this.throwNotFound(identifier);
|
|
191
191
|
}
|
|
192
|
-
this.emitDetail(record, flags);
|
|
192
|
+
await this.emitDetail(record, flags);
|
|
193
193
|
}
|
|
194
194
|
/**
|
|
195
195
|
* Per-subclass detail-mode hints. Return runnable commands pointing to
|
|
@@ -208,11 +208,17 @@ export class InspectResourceCommand extends SwellCommand {
|
|
|
208
208
|
* `--json` is the machine-readable contract — hints belong in the same
|
|
209
209
|
* stream as the human-facing output and are simply omitted there.
|
|
210
210
|
*/
|
|
211
|
-
emitDetail(record, flags) {
|
|
211
|
+
emitDetail(record, flags, notes = []) {
|
|
212
212
|
this.log(JSON.stringify(record, null, 2));
|
|
213
213
|
if (flags.json) {
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
|
+
if (notes.length > 0) {
|
|
217
|
+
this.log();
|
|
218
|
+
for (const note of notes) {
|
|
219
|
+
this.log(note);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
216
222
|
const lines = this.hints(record);
|
|
217
223
|
if (lines.length === 0) {
|
|
218
224
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function errorReasons(errors: unknown, path?: string[]): string[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Every reason an API error answer gives, each with the path of the field it
|
|
2
|
+
// is about: errors come as { field: { message } }, nested, as strings or as
|
|
3
|
+
// { error } without a field
|
|
4
|
+
export function errorReasons(errors, path = []) {
|
|
5
|
+
if (typeof errors === 'string') {
|
|
6
|
+
return [formatReason(path, errors)];
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(errors)) {
|
|
9
|
+
return errors.flatMap((item) => errorReasons(item, path));
|
|
10
|
+
}
|
|
11
|
+
if (!errors || typeof errors !== 'object') {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const { message, ...fields } = errors;
|
|
15
|
+
if (typeof message === 'string') {
|
|
16
|
+
return [formatReason(path, message)];
|
|
17
|
+
}
|
|
18
|
+
return Object.entries(fields).flatMap(([key, value]) =>
|
|
19
|
+
// An error's code names its kind, not a reason
|
|
20
|
+
key === 'code' && typeof value === 'string'
|
|
21
|
+
? []
|
|
22
|
+
: // Developers know an app's private_id as the id in swell.json
|
|
23
|
+
errorReasons(value, [...path, key === 'private_id' ? 'id' : key]));
|
|
24
|
+
}
|
|
25
|
+
function formatReason(path, message) {
|
|
26
|
+
const fieldPath = path.filter((key) => key !== 'error').join('.');
|
|
27
|
+
return fieldPath ? `${fieldPath}: ${message}` : message;
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Values = Record<string, any>;
|
|
2
|
+
export type ActionSource = 'bulk' | 'field' | 'list' | 'record' | 'settings';
|
|
3
|
+
/** An app function action, with what the dashboard sends to run it */
|
|
4
|
+
export interface DeclaredAction {
|
|
5
|
+
function: string;
|
|
6
|
+
id: string;
|
|
7
|
+
modal?: Values;
|
|
8
|
+
/** The query of the view whose records a bulk action runs on */
|
|
9
|
+
query?: Values;
|
|
10
|
+
/** The name of the settings config, for a settings action or action field */
|
|
11
|
+
settings?: string;
|
|
12
|
+
source: ActionSource;
|
|
13
|
+
view?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The function actions a content config declares.
|
|
17
|
+
*
|
|
18
|
+
* @param values the values of the content config
|
|
19
|
+
* @returns the actions
|
|
20
|
+
*/
|
|
21
|
+
export declare function getContentActions(values: Values): DeclaredAction[];
|
|
22
|
+
/**
|
|
23
|
+
* The function actions a settings config declares.
|
|
24
|
+
*
|
|
25
|
+
* @param values the values of the settings config
|
|
26
|
+
* @param settings the name of the settings config
|
|
27
|
+
* @returns the actions
|
|
28
|
+
*/
|
|
29
|
+
export declare function getSettingsActions(values: Values, settings: string): DeclaredAction[];
|
|
30
|
+
/**
|
|
31
|
+
* The ids of the fields of an action's modal, as the platform finds them to
|
|
32
|
+
* pass on only their values.
|
|
33
|
+
*
|
|
34
|
+
* @param modal the modal of the action
|
|
35
|
+
* @returns the field ids
|
|
36
|
+
*/
|
|
37
|
+
export declare function getModalFieldIds(modal: Values): string[];
|
|
38
|
+
/**
|
|
39
|
+
* A function name as the platform compares an action's `function` with it.
|
|
40
|
+
*
|
|
41
|
+
* @param name a function name
|
|
42
|
+
* @returns the name in the form the platform compares
|
|
43
|
+
*/
|
|
44
|
+
export declare function functionSlug(name: string): string;
|
|
45
|
+
export {};
|