@swell/cli 2.9.32 → 2.9.34

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 (43) hide show
  1. package/dist/commands/app/dev.js +2 -1
  2. package/dist/commands/app/frontend/dev.js +4 -2
  3. package/dist/commands/app/push.d.ts +1 -0
  4. package/dist/commands/app/push.js +38 -3
  5. package/dist/commands/app/version.js +3 -0
  6. package/dist/commands/create/function.js +14 -4
  7. package/dist/commands/create/model.js +9 -24
  8. package/dist/commands/inspect/components.d.ts +31 -0
  9. package/dist/commands/inspect/components.js +110 -0
  10. package/dist/commands/inspect/extensions.js +18 -9
  11. package/dist/commands/inspect/frontend.d.ts +37 -0
  12. package/dist/commands/inspect/frontend.js +75 -0
  13. package/dist/commands/inspect/functions.d.ts +11 -1
  14. package/dist/commands/inspect/functions.js +89 -6
  15. package/dist/commands/inspect/index.js +2 -0
  16. package/dist/inspect-resource-command.d.ts +1 -1
  17. package/dist/inspect-resource-command.js +8 -2
  18. package/dist/lib/api-errors.d.ts +1 -0
  19. package/dist/lib/api-errors.js +28 -0
  20. package/dist/lib/app-config.js +1 -0
  21. package/dist/lib/apps/components.d.ts +7 -0
  22. package/dist/lib/apps/components.js +10 -0
  23. package/dist/lib/apps/extension-manifest.d.ts +1 -0
  24. package/dist/lib/apps/extension-manifest.js +43 -0
  25. package/dist/lib/apps/index.d.ts +5 -0
  26. package/dist/lib/apps/index.js +31 -0
  27. package/dist/lib/apps/paths.d.ts +8 -0
  28. package/dist/lib/apps/paths.js +34 -4
  29. package/dist/lib/bundle.js +15 -2
  30. package/dist/lib/create/function.d.ts +1 -1
  31. package/dist/lib/create/function.js +9 -5
  32. package/dist/lib/create/index.js +2 -1
  33. package/dist/lib/create/model.d.ts +7 -3
  34. package/dist/lib/create/model.js +67 -9
  35. package/dist/lib/create/tests.js +4 -2
  36. package/dist/lib/inspect/extensions.d.ts +4 -3
  37. package/dist/lib/inspect/extensions.js +36 -5
  38. package/dist/lib/swell-function-wrapper.js +5 -4
  39. package/dist/push-app-command.js +32 -19
  40. package/dist/remote-app-command.d.ts +1 -1
  41. package/dist/remote-app-command.js +78 -47
  42. package/oclif.manifest.json +139 -6
  43. package/package.json +1 -1
@@ -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
- if (!(await this.ensureAppExists(undefined, false))) {
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
- if (!(await this.ensureAppExists(undefined, false))) {
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 (!noPush && !isAppDev) {
50
+ if (isPushing) {
49
51
  await this.pushAppConfigs();
50
52
  }
51
53
  if (this.app.type === 'storefront') {
@@ -30,5 +30,6 @@ export default class AppPush extends PushAppCommand {
30
30
  run(): Promise<void>;
31
31
  protected pushesSwellJson(file?: string): boolean;
32
32
  private checkActionFunctions;
33
+ private enableAutoDisabledFunctions;
33
34
  private exitIfFilesFailed;
34
35
  }
@@ -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
- if (!noDeploy && affectsFrontendDeployment(relativePath)) {
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 route trigger.', { exit: 1 });
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:string -l "Visitors" -y',
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,price:currency)',
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 invalid = [];
77
- for (const pair of pairs) {
78
- if (!pair.includes(':')) {
79
- invalid.push(pair);
80
- continue;
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('/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