@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.
Files changed (57) hide show
  1. package/dist/commands/app/action.d.ts +25 -0
  2. package/dist/commands/app/action.js +233 -0
  3. package/dist/commands/app/dev.js +2 -1
  4. package/dist/commands/app/frontend/deploy.d.ts +1 -0
  5. package/dist/commands/app/frontend/deploy.js +4 -0
  6. package/dist/commands/app/frontend/dev.js +4 -2
  7. package/dist/commands/app/push.d.ts +3 -0
  8. package/dist/commands/app/push.js +100 -4
  9. package/dist/commands/app/version.js +3 -0
  10. package/dist/commands/create/function.js +14 -4
  11. package/dist/commands/create/model.js +9 -24
  12. package/dist/commands/create/webhook.js +7 -13
  13. package/dist/commands/inspect/components.d.ts +31 -0
  14. package/dist/commands/inspect/components.js +110 -0
  15. package/dist/commands/inspect/extensions.js +18 -9
  16. package/dist/commands/inspect/frontend.d.ts +37 -0
  17. package/dist/commands/inspect/frontend.js +75 -0
  18. package/dist/commands/inspect/functions.d.ts +11 -1
  19. package/dist/commands/inspect/functions.js +89 -6
  20. package/dist/commands/inspect/index.js +2 -0
  21. package/dist/commands/schema.js +5 -0
  22. package/dist/help/custom-help.js +5 -1
  23. package/dist/inspect-resource-command.d.ts +1 -1
  24. package/dist/inspect-resource-command.js +8 -2
  25. package/dist/lib/api-errors.d.ts +1 -0
  26. package/dist/lib/api-errors.js +28 -0
  27. package/dist/lib/apps/actions.d.ts +45 -0
  28. package/dist/lib/apps/actions.js +114 -0
  29. package/dist/lib/apps/components.d.ts +7 -0
  30. package/dist/lib/apps/components.js +10 -0
  31. package/dist/lib/apps/extension-manifest.d.ts +1 -0
  32. package/dist/lib/apps/extension-manifest.js +43 -0
  33. package/dist/lib/apps/index.d.ts +1 -0
  34. package/dist/lib/apps/index.js +30 -0
  35. package/dist/lib/apps/listing.d.ts +1 -0
  36. package/dist/lib/apps/listing.js +41 -0
  37. package/dist/lib/apps/paths.d.ts +8 -0
  38. package/dist/lib/apps/paths.js +34 -4
  39. package/dist/lib/bundle.js +26 -41
  40. package/dist/lib/create/function.d.ts +1 -1
  41. package/dist/lib/create/function.js +9 -5
  42. package/dist/lib/create/index.js +2 -1
  43. package/dist/lib/create/model.d.ts +7 -3
  44. package/dist/lib/create/model.js +67 -9
  45. package/dist/lib/function-declarations.d.ts +8 -0
  46. package/dist/lib/function-declarations.js +27 -0
  47. package/dist/lib/inspect/extensions.d.ts +4 -3
  48. package/dist/lib/inspect/extensions.js +36 -5
  49. package/dist/lib/swell-function-wrapper.js +13 -5
  50. package/dist/lib/swell-workflow-request.d.ts +34 -0
  51. package/dist/lib/swell-workflow-request.js +58 -0
  52. package/dist/push-app-command.d.ts +2 -1
  53. package/dist/push-app-command.js +43 -22
  54. package/dist/remote-app-command.d.ts +2 -2
  55. package/dist/remote-app-command.js +79 -51
  56. package/oclif.manifest.json +263 -10
  57. 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('/data/settings/payments'),
81
- this.fetchSafe('/data/settings/shipments'),
82
- this.fetchSafe('/data/settings/taxes'),
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('/data/settings/payments'),
282
- this.fetchSafe('/data/settings/shipments'),
283
- this.fetchSafe('/data/settings/taxes'),
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
- async fetchSafe(adminPath) {
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({ adminPath });
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 failure).
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 (!r.enabled) {
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
- if (r.date_final_attempt) {
67
- parts.push(`last fail ${r.date_final_attempt}`);
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.');
@@ -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) {
@@ -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
- return [name, `${shortChar}--${name}${suffix}`];
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 {};