@swell/cli 2.6.0 → 2.7.1

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 (46) hide show
  1. package/dist/commands/app/frontend/dev.js +18 -0
  2. package/dist/commands/inspect/content.d.ts +25 -14
  3. package/dist/commands/inspect/content.js +34 -144
  4. package/dist/commands/inspect/extensions.d.ts +49 -0
  5. package/dist/commands/inspect/extensions.js +424 -0
  6. package/dist/commands/inspect/functions.d.ts +48 -0
  7. package/dist/commands/inspect/functions.js +83 -0
  8. package/dist/commands/inspect/index.js +8 -7
  9. package/dist/commands/inspect/models.d.ts +17 -2
  10. package/dist/commands/inspect/models.js +114 -47
  11. package/dist/commands/inspect/notifications.d.ts +31 -0
  12. package/dist/commands/inspect/notifications.js +125 -0
  13. package/dist/commands/inspect/settings.d.ts +28 -0
  14. package/dist/commands/inspect/settings.js +82 -0
  15. package/dist/commands/inspect/webhooks.d.ts +34 -0
  16. package/dist/commands/inspect/webhooks.js +61 -0
  17. package/dist/create-app-command.d.ts +7 -0
  18. package/dist/create-app-command.js +80 -9
  19. package/dist/inspect-resource-command.d.ts +91 -0
  20. package/dist/inspect-resource-command.js +232 -0
  21. package/dist/lib/apps/index.d.ts +2 -1
  22. package/dist/lib/apps/index.js +43 -6
  23. package/dist/lib/apps/inspect-scope.d.ts +58 -0
  24. package/dist/lib/apps/inspect-scope.js +60 -0
  25. package/dist/lib/apps/object-id.d.ts +7 -0
  26. package/dist/lib/apps/object-id.js +9 -0
  27. package/dist/lib/apps/paths.js +8 -1
  28. package/dist/lib/apps/resolve.d.ts +16 -0
  29. package/dist/lib/apps/resolve.js +39 -0
  30. package/dist/lib/apps/slug.d.ts +29 -0
  31. package/dist/lib/apps/slug.js +12 -0
  32. package/dist/lib/inspect/content.d.ts +39 -0
  33. package/dist/lib/inspect/content.js +76 -0
  34. package/dist/lib/inspect/extensions.d.ts +267 -0
  35. package/dist/lib/inspect/extensions.js +690 -0
  36. package/dist/lib/inspect/notifications.d.ts +115 -0
  37. package/dist/lib/inspect/notifications.js +173 -0
  38. package/dist/lib/inspect/settings.d.ts +61 -0
  39. package/dist/lib/inspect/settings.js +56 -0
  40. package/dist/lib/inspect/table.d.ts +29 -0
  41. package/dist/lib/inspect/table.js +61 -0
  42. package/dist/push-app-command.js +3 -2
  43. package/dist/swell-api-command.d.ts +0 -4
  44. package/dist/swell-api-command.js +2 -18
  45. package/oclif.manifest.json +392 -35
  46. package/package.json +1 -1
@@ -0,0 +1,424 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { FetchError } from 'node-fetch';
3
+ import { inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
4
+ import { getCurrentAppSlugId, hasAppContext } from '../../lib/apps/index.js';
5
+ import { resolveInspectScope, ScopeError, } from '../../lib/apps/inspect-scope.js';
6
+ import { HEX24, isObjectId } from '../../lib/apps/object-id.js';
7
+ import { resolveAppId } from '../../lib/apps/resolve.js';
8
+ import { default as localConfig } from '../../lib/config.js';
9
+ import { buildExtensionDetail, buildOrphanDetail, componentMatchesExtension, diffManifestEntries, formatExtensionKey, functionMatchesExtension, listGroupForApp, listGroupForOrphans, listMetaFor, nextStepLines, orphanListMeta, parseExtensionKey, } from '../../lib/inspect/extensions.js';
10
+ import { renderKeyMetaTable } from '../../lib/inspect/table.js';
11
+ import { SwellCommand } from '../../swell-command.js';
12
+ export default class Extensions extends SwellCommand {
13
+ static summary = 'Platform extensions with activation chain.';
14
+ static description = `Lists declared payment, shipping, and tax extensions across deployed apps, with status (activated, not activated, gateway missing, etc.) and bound function/component counts. Pass --app=<slug> or --app=. (current swell.json) to scope.
15
+
16
+ Pass an identifier to view a single extension as a synthesized JSON envelope (manifest + native_bindings + bound functions/components + required vs missing events), followed by a "Next steps" footer mixing runnable commands and merchant-UI instructions (prefixed "(merchant)").
17
+
18
+ Identifier forms:
19
+ app.<app>.<extId> — full paste-back key (list column 1)
20
+ <extId> — requires --app= scope
21
+
22
+ 24-char hex id is not accepted: extensions are a synthesized resource with no canonical record id.
23
+ `;
24
+ static args = { ...inspectResourceBaseArgs };
25
+ static flags = { ...inspectResourceBaseFlags };
26
+ static examples = [
27
+ 'swell inspect extensions',
28
+ 'swell inspect extensions --app=my-app',
29
+ 'swell inspect extensions app.my-app.revolut',
30
+ 'swell inspect extensions revolut --app=my-app',
31
+ 'swell inspect extensions app.my-app.revolut --json',
32
+ 'swell inspect extensions --live',
33
+ ];
34
+ async run() {
35
+ const { args, flags } = await this.parse(Extensions);
36
+ if (flags.json && !args.identifier) {
37
+ this.error('--json is only valid in detail mode. Pass an identifier to inspect a single extension, or drop --json for the list view.', { exit: 1 });
38
+ }
39
+ if (!flags.live) {
40
+ await this.api.setEnv('test');
41
+ }
42
+ const scope = await resolveInspectScope({
43
+ resolveAppId: (slug) => resolveAppId(this.api, slug),
44
+ readCurrentAppSlug: () => getCurrentAppSlugId(),
45
+ }, { app: flags.app });
46
+ if (args.identifier) {
47
+ await this.showDetail(args.identifier, scope, flags);
48
+ return;
49
+ }
50
+ await this.showList(scope, flags);
51
+ }
52
+ async catch(error) {
53
+ if (error instanceof FetchError) {
54
+ const message = `Could not connect to Swell API. Please try again later: ${error.message}`;
55
+ return this.error(message, { exit: 2, code: error.code });
56
+ }
57
+ if (error instanceof ScopeError) {
58
+ return this.error(error.message, { exit: 1 });
59
+ }
60
+ return this.error(error.message, { exit: 1 });
61
+ }
62
+ /**
63
+ * List mode is bounded to two parallel round-trip phases regardless of app
64
+ * count: phase 1 fans `/client/apps`, the three settings singletons, and a
65
+ * single `/data/:functions` filter; phase 2 fans `/apps/<id>/configs?type=component`
66
+ * only for apps with a non-empty `extensions[]`.
67
+ *
68
+ * `include` / `aggregate` are NOT used here — the CLI's `node-fetch`
69
+ * transport cannot transmit GET-with-body, and URL-form transmission of
70
+ * those payloads is silently dropped server-side. Verified empirically
71
+ * against the live admin API. Don't reach for those primitives.
72
+ */
73
+ async showList(scope, flags) {
74
+ const fnQuery = { extension: { $exists: true } };
75
+ if (scope.appId) {
76
+ fnQuery.app_id = scope.appId;
77
+ }
78
+ const [installedAppsResp, payments, shipments, taxes, functionsResp] = await Promise.all([
79
+ this.api.get({ adminPath: '/client/apps' }),
80
+ this.fetchSafe('/data/settings/payments'),
81
+ this.fetchSafe('/data/settings/shipments'),
82
+ this.fetchSafe('/data/settings/taxes'),
83
+ this.api.getAll({ adminPath: '/data/:functions' }, { query: fnQuery }),
84
+ ]);
85
+ const installed = (installedAppsResp?.results ??
86
+ []);
87
+ const appsScoped = scope.appId
88
+ ? installed.filter((a) => a.app_id === scope.appId)
89
+ : installed;
90
+ const appSlugById = {};
91
+ for (const a of appsScoped) {
92
+ const slug = slugFromInstalled(a);
93
+ if (slug)
94
+ appSlugById[a.app_id] = slug;
95
+ }
96
+ // Phase 2: components per app with a non-empty extensions[].
97
+ const appsWithExtensions = appsScoped.filter((a) => (a.app?.extensions ?? []).length > 0);
98
+ const componentResponses = await Promise.all(appsWithExtensions.map((a) => this.api
99
+ .getAll({ adminPath: `/apps/${a.app_id}/configs` }, { query: { type: 'component' } })
100
+ .catch(() => ({ results: [] }))));
101
+ const componentsByAppId = {};
102
+ for (const [idx, a] of appsWithExtensions.entries()) {
103
+ componentsByAppId[a.app_id] =
104
+ componentResponses[idx]?.results ?? [];
105
+ }
106
+ const allFunctions = (functionsResp?.results ?? []);
107
+ // Build per-app, per-extension row state.
108
+ const rowStates = [];
109
+ const orphans = new Map();
110
+ for (const installedApp of appsScoped) {
111
+ const appId = installedApp.app_id;
112
+ const appSlug = appSlugById[appId] ?? appId;
113
+ const declared = installedApp.app?.extensions ?? [];
114
+ const declaredIds = new Set(declared.map((e) => e.id));
115
+ const appFunctions = allFunctions.filter((f) => f.app_id === appId);
116
+ const appComponents = componentsByAppId[appId] ?? [];
117
+ for (const entry of declared) {
118
+ rowStates.push({
119
+ appId,
120
+ appSlug,
121
+ manifest: entry,
122
+ functions: appFunctions.filter((f) => functionMatchesExtension(f, entry.id)),
123
+ components: appComponents.filter((c) => componentMatchesExtension(c, entry.id)),
124
+ });
125
+ }
126
+ for (const fn of appFunctions) {
127
+ if (!fn.extension)
128
+ continue;
129
+ if (declaredIds.has(fn.extension))
130
+ continue;
131
+ const key = `${appId}::${fn.extension}`;
132
+ const existing = orphans.get(key);
133
+ if (existing) {
134
+ existing.functions.push(fn);
135
+ }
136
+ else {
137
+ orphans.set(key, {
138
+ appId,
139
+ appSlug,
140
+ unresolvedId: fn.extension,
141
+ functions: [fn],
142
+ components: [],
143
+ });
144
+ }
145
+ }
146
+ for (const comp of appComponents) {
147
+ const ext = comp.values?.extension;
148
+ if (!ext)
149
+ continue;
150
+ if (declaredIds.has(ext))
151
+ continue;
152
+ const key = `${appId}::${ext}`;
153
+ const existing = orphans.get(key);
154
+ if (existing) {
155
+ existing.components.push(comp);
156
+ }
157
+ else {
158
+ orphans.set(key, {
159
+ appId,
160
+ appSlug,
161
+ unresolvedId: ext,
162
+ functions: [],
163
+ components: [comp],
164
+ });
165
+ }
166
+ }
167
+ }
168
+ // --app=. surfaces local-only extensions as `not deployed` rows.
169
+ if (flags.app === '.') {
170
+ const local = await readLocalManifest();
171
+ const localId = local?.id;
172
+ const localExtensions = local?.extensions ?? [];
173
+ if (localId) {
174
+ const installedForLocal = appsScoped.find((a) => slugFromInstalled(a) === localId || a.app_id === localId);
175
+ const appId = installedForLocal?.app_id ?? localId;
176
+ const deployedIds = new Set((installedForLocal?.app?.extensions ?? []).map((e) => e.id));
177
+ for (const entry of localExtensions) {
178
+ if (deployedIds.has(entry.id))
179
+ continue;
180
+ rowStates.push({
181
+ appId,
182
+ appSlug: localId,
183
+ manifest: entry,
184
+ functions: [],
185
+ components: [],
186
+ notDeployed: true,
187
+ });
188
+ }
189
+ }
190
+ }
191
+ this.printPreamble(flags.live ?? false);
192
+ if (rowStates.length === 0 && orphans.size === 0) {
193
+ this.log();
194
+ this.log(' (no extensions found)');
195
+ this.log();
196
+ return;
197
+ }
198
+ rowStates.sort((a, b) => {
199
+ const cmp = a.appSlug.localeCompare(b.appSlug);
200
+ return cmp === 0 ? a.manifest.id.localeCompare(b.manifest.id) : cmp;
201
+ });
202
+ const rows = rowStates.map((state) => {
203
+ const detail = buildExtensionDetail({
204
+ appId: state.appId,
205
+ appSlug: state.appSlug,
206
+ extId: state.manifest.id,
207
+ manifest: state.manifest,
208
+ notDeployed: state.notDeployed,
209
+ payments,
210
+ shipments,
211
+ taxes,
212
+ functions: state.functions,
213
+ components: state.components,
214
+ });
215
+ return {
216
+ key: formatExtensionKey(state.appSlug, state.manifest.id),
217
+ meta: listMetaFor(detail),
218
+ group: listGroupForApp(state.appSlug),
219
+ };
220
+ });
221
+ const orphanRows = [...orphans.values()]
222
+ .sort((a, b) => {
223
+ const cmp = a.appSlug.localeCompare(b.appSlug);
224
+ return cmp === 0 ? a.unresolvedId.localeCompare(b.unresolvedId) : cmp;
225
+ })
226
+ .map((state) => ({
227
+ key: formatExtensionKey(state.appSlug, state.unresolvedId),
228
+ meta: orphanListMeta(state.functions.length, state.components.length),
229
+ group: listGroupForOrphans(),
230
+ }));
231
+ this.log();
232
+ for (const line of renderKeyMetaTable([...rows, ...orphanRows])) {
233
+ this.log(line);
234
+ }
235
+ this.log();
236
+ this.log('Run "swell inspect extensions <key>" to view an extension.');
237
+ this.log();
238
+ }
239
+ /**
240
+ * Detail mode is bounded to one round-trip's worth of latency: the install
241
+ * record (or `/apps/<id>` fallback), the relevant settings singleton(s),
242
+ * `/data/:functions?where[app_id]=<id>&where[extension]=<extId>`, and
243
+ * `/apps/<id>/configs?type=component` — all in parallel.
244
+ */
245
+ async showDetail(identifier, scope, flags) {
246
+ if (HEX24.test(identifier)) {
247
+ this.error(`Extensions are a synthesized resource with no 24-char id. ` +
248
+ `Pass app.<slug>.<extId> or a bare extension id with --app=.`, { exit: 1 });
249
+ }
250
+ const parsed = parseExtensionKey(identifier);
251
+ let appId;
252
+ let appSlug;
253
+ let extId;
254
+ switch (parsed.kind) {
255
+ case 'slug': {
256
+ const isHex = isObjectId(parsed.appPart);
257
+ appId = isHex
258
+ ? parsed.appPart
259
+ : await resolveAppId(this.api, parsed.appPart);
260
+ appSlug = isHex ? parsed.appPart : parsed.appPart;
261
+ extId = parsed.extId;
262
+ break;
263
+ }
264
+ case 'name': {
265
+ if (!scope.appId) {
266
+ this.error(`Bare extension id '${identifier}' requires --app=<slug> or --app=. to scope. ` +
267
+ `Alternatively, pass a full key (app.<slug>.<extId>).`, { exit: 1 });
268
+ }
269
+ appId = scope.appId;
270
+ appSlug = scope.appSlug ?? scope.appId;
271
+ extId = parsed.name;
272
+ break;
273
+ }
274
+ case 'invalid': {
275
+ this.error(`Invalid extension identifier '${identifier}'. ` +
276
+ `Expected bare id or app.<slug>.<extId>.`, { exit: 1 });
277
+ }
278
+ }
279
+ const [installedAppsResp, payments, shipments, taxes, functionsResp] = await Promise.all([
280
+ 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.api.getAll({ adminPath: '/data/:functions' }, { query: { app_id: appId, extension: extId } }),
285
+ ]);
286
+ const installed = (installedAppsResp?.results ??
287
+ []);
288
+ const installedApp = installed.find((a) => a.app_id === appId);
289
+ let manifest = installedApp?.app?.extensions?.find((e) => e.id === extId) ?? null;
290
+ // Fallback to /apps/<id> if the install record's nested manifest is absent.
291
+ if (!manifest) {
292
+ const fallback = await this.api
293
+ .get({ adminPath: `/apps/${appId}` })
294
+ .catch(() => null);
295
+ manifest = fallback?.extensions?.find((e) => e.id === extId);
296
+ manifest ??= null;
297
+ }
298
+ const componentsResp = await this.api
299
+ .getAll({ adminPath: `/apps/${appId}/configs` }, { query: { type: 'component' } })
300
+ .catch(() => ({ results: [] }));
301
+ const allFunctions = (functionsResp?.results ?? []);
302
+ const allComponents = (componentsResp?.results ?? []);
303
+ const declaredIds = new Set((installedApp?.app?.extensions ?? []).map((e) => e.id));
304
+ let detail;
305
+ if (manifest) {
306
+ const fnsForExtension = allFunctions.filter((f) => functionMatchesExtension(f, extId));
307
+ const compsForExtension = allComponents.filter((c) => componentMatchesExtension(c, extId));
308
+ let localDiff = null;
309
+ if (flags.app === '.' && hasAppContext()) {
310
+ const local = await readLocalManifest();
311
+ const localEntry = local?.extensions?.find((e) => e.id === extId);
312
+ if (localEntry) {
313
+ localDiff = diffManifestEntries(localEntry, manifest);
314
+ }
315
+ }
316
+ detail = buildExtensionDetail({
317
+ appId,
318
+ appSlug,
319
+ extId,
320
+ manifest,
321
+ payments,
322
+ shipments,
323
+ taxes,
324
+ functions: fnsForExtension,
325
+ components: compsForExtension,
326
+ localDiff,
327
+ });
328
+ }
329
+ else {
330
+ // Possible orphan: function/component exists with this `extension` value
331
+ // but no manifest entry declares it. Surface as the orphan envelope only
332
+ // when we actually find handlers; otherwise it's a "not found" lookup.
333
+ const orphanFns = allFunctions.filter((f) => f.extension === extId);
334
+ const orphanComps = allComponents.filter((c) => c.values?.extension === extId);
335
+ if (orphanFns.length === 0 && orphanComps.length === 0) {
336
+ this.error(`No extension '${extId}' declared by app '${appSlug}', and no handlers reference it. ` +
337
+ `List available: swell inspect extensions${scope.appSlug ? ` --app=${scope.appSlug}` : ''}`, { exit: 1 });
338
+ }
339
+ // Local-not-deployed: under --app=. with a local manifest entry that
340
+ // hasn't been deployed yet, surface `not deployed` rather than orphan.
341
+ let localEntry = null;
342
+ if (flags.app === '.' && hasAppContext()) {
343
+ const local = await readLocalManifest();
344
+ localEntry = local?.extensions?.find((e) => e.id === extId) ?? null;
345
+ }
346
+ detail =
347
+ localEntry && !declaredIds.has(extId)
348
+ ? buildExtensionDetail({
349
+ appId,
350
+ appSlug,
351
+ extId,
352
+ manifest: localEntry,
353
+ notDeployed: true,
354
+ payments,
355
+ shipments,
356
+ taxes,
357
+ functions: orphanFns,
358
+ components: orphanComps,
359
+ })
360
+ : buildOrphanDetail({
361
+ appId,
362
+ appSlug,
363
+ unresolvedId: extId,
364
+ functions: orphanFns,
365
+ components: orphanComps,
366
+ });
367
+ }
368
+ this.emitDetail(detail, flags);
369
+ }
370
+ /**
371
+ * `extensions` emits a synthesized envelope rather than a raw API record —
372
+ * other inspect subcommands print one upstream record, but here the JSON
373
+ * nests `manifest`, `native_bindings[].record`, `bound.functions[]`, and
374
+ * `bound.components[]` under shared status fields.
375
+ *
376
+ * The `Next steps:` footer mixes runnable shell commands and merchant-UI
377
+ * lines (prefixed `(merchant)`). The structured `action` and `action_owner`
378
+ * fields above give an agent a parseable signal independent of the footer.
379
+ */
380
+ emitDetail(detail, flags) {
381
+ this.log(JSON.stringify(detail, null, 2));
382
+ if (flags.json) {
383
+ return;
384
+ }
385
+ const lines = nextStepLines(detail);
386
+ if (lines.length === 0) {
387
+ return;
388
+ }
389
+ this.log();
390
+ this.log('Next steps:');
391
+ for (const line of lines) {
392
+ this.log(` ${line}`);
393
+ }
394
+ this.log();
395
+ }
396
+ async fetchSafe(adminPath) {
397
+ try {
398
+ const result = await this.api.get({ adminPath });
399
+ return (result ?? null);
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ printPreamble(live) {
406
+ const store = localConfig.getDefaultStore();
407
+ const envLabel = live ? '[live]' : '[test]';
408
+ this.log(`Extensions in '${store}' ${envLabel}`);
409
+ }
410
+ }
411
+ function slugFromInstalled(a) {
412
+ return a.app_private_id?.replace(/^_/, '') || a.app_public_id;
413
+ }
414
+ async function readLocalManifest() {
415
+ if (!hasAppContext())
416
+ return null;
417
+ try {
418
+ const raw = await fs.readFile('swell.json', 'utf8');
419
+ return JSON.parse(raw);
420
+ }
421
+ catch {
422
+ return null;
423
+ }
424
+ }
@@ -0,0 +1,48 @@
1
+ import { InspectResourceCommand } from '../../inspect-resource-command.js';
2
+ interface FunctionRecord {
3
+ id?: string;
4
+ name?: string;
5
+ enabled?: boolean;
6
+ cron?: {
7
+ schedule?: string;
8
+ };
9
+ route?: {
10
+ headers?: string[];
11
+ methods?: string[];
12
+ public?: boolean;
13
+ };
14
+ model?: {
15
+ events?: string[];
16
+ };
17
+ date_cron_scheduled?: string;
18
+ date_final_attempt?: string;
19
+ app_id?: string;
20
+ }
21
+ export default class Functions extends InspectResourceCommand {
22
+ static summary: string;
23
+ static description: string;
24
+ static args: {
25
+ identifier: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
26
+ };
27
+ static flags: {
28
+ app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
29
+ live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
30
+ json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
31
+ yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
32
+ };
33
+ static examples: string[];
34
+ protected resourceLabel: string;
35
+ protected resourceLabelSingular: string;
36
+ protected adminPath: string;
37
+ protected commandName: string;
38
+ run(): Promise<void>;
39
+ protected metaFor(r: FunctionRecord): string | undefined;
40
+ /**
41
+ * Model-triggered functions emit `/events:webhooks` rows on each invocation.
42
+ * Route and cron functions do not — pointing the agent at an empty query
43
+ * would teach them their function isn't running. The logs hint always
44
+ * applies as long as we have a function name.
45
+ */
46
+ protected hints(record: FunctionRecord): string[];
47
+ }
48
+ export {};
@@ -0,0 +1,83 @@
1
+ import { InspectResourceCommand, inspectResourceBaseArgs, inspectResourceBaseFlags, } from '../../inspect-resource-command.js';
2
+ function deriveTrigger(r) {
3
+ if (r.cron?.schedule) {
4
+ return `cron (${r.cron.schedule})`;
5
+ }
6
+ if (r.model?.events?.length) {
7
+ const n = r.model.events.length;
8
+ return `model (${n} event${n === 1 ? '' : 's'})`;
9
+ }
10
+ if (r.route) {
11
+ const { methods } = r.route;
12
+ if (!methods?.length)
13
+ return 'route';
14
+ return methods.length <= 2
15
+ ? `route (${methods.join(',')})`
16
+ : `route (${methods.length} methods)`;
17
+ }
18
+ return undefined;
19
+ }
20
+ export default class Functions extends InspectResourceCommand {
21
+ static summary = 'Functions with scheduling and trigger state.';
22
+ static description = `Lists functions across all apps, grouped by app. Pass --app=<slug> or --app=. (current swell.json) to scope.
23
+
24
+ List output: paste-back key + status (disabled, trigger, next cron run, last failure).
25
+
26
+ Pass an identifier to view a single record as JSON.
27
+
28
+ Identifier forms:
29
+ app.<app>.<name> — full paste-back key (list column 1)
30
+ <name> — requires --app= scope
31
+ <24-char id> — any scope
32
+ `;
33
+ static args = { ...inspectResourceBaseArgs };
34
+ static flags = { ...inspectResourceBaseFlags };
35
+ static examples = [
36
+ 'swell inspect functions',
37
+ 'swell inspect functions --app=my-app',
38
+ 'swell inspect functions app.my-app.payment-sync',
39
+ 'swell inspect functions payment-sync --app=my-app',
40
+ 'swell inspect functions --live',
41
+ ];
42
+ resourceLabel = 'Functions';
43
+ resourceLabelSingular = 'function';
44
+ adminPath = '/data/:functions';
45
+ commandName = 'functions';
46
+ async run() {
47
+ const { args, flags } = await this.parse(Functions);
48
+ await this.runInspect({ args, flags });
49
+ }
50
+ metaFor(r) {
51
+ const parts = [];
52
+ if (!r.enabled) {
53
+ parts.push('disabled');
54
+ }
55
+ const trigger = deriveTrigger(r);
56
+ if (trigger) {
57
+ parts.push(trigger);
58
+ }
59
+ if (r.date_cron_scheduled) {
60
+ parts.push(`next ${r.date_cron_scheduled}`);
61
+ }
62
+ if (r.date_final_attempt) {
63
+ parts.push(`last fail ${r.date_final_attempt}`);
64
+ }
65
+ return parts.length > 0 ? parts.join(' · ') : undefined;
66
+ }
67
+ /**
68
+ * Model-triggered functions emit `/events:webhooks` rows on each invocation.
69
+ * Route and cron functions do not — pointing the agent at an empty query
70
+ * would teach them their function isn't running. The logs hint always
71
+ * applies as long as we have a function name.
72
+ */
73
+ hints(record) {
74
+ const lines = [];
75
+ if (record.model?.events?.length && record.id) {
76
+ lines.push(`swell api get '/events:webhooks?where[function_id]=${record.id}&limit=10'`);
77
+ }
78
+ if (record.name) {
79
+ lines.push(`swell logs --type function -s '${record.name}'`);
80
+ }
81
+ return lines;
82
+ }
83
+ }
@@ -1,16 +1,17 @@
1
1
  import { Command } from '@oclif/core';
2
2
  export default class Inspect extends Command {
3
3
  static summary = 'Inspect deployed artifacts in your store.';
4
- static description = `View configuration and data for deployed resources.
5
-
6
- Inspect models, content views, and other deployed artifacts in your Swell store.`;
4
+ static description = `Subcommands list globally and group by app. Pass --app=<slug> or --app=. (current swell.json) to scope. Pass an identifier to print the full record as JSON, followed by a "Next steps" footer of related commands. Use --json to omit the footer for piping.
5
+ `;
7
6
  static examples = [
8
- 'swell inspect models',
9
7
  'swell inspect models /products',
10
- 'swell inspect models /apps/myapp/orders',
8
+ 'swell inspect content app.honest_reviews.reviews',
9
+ 'swell inspect webhooks --app=my-app',
10
+ 'swell inspect functions app.my-app.payment-sync',
11
+ 'swell inspect notifications com.orders.receipt.v2',
12
+ 'swell inspect settings app.my-app',
11
13
  ];
12
14
  async run() {
13
- this.log('Use "swell inspect models" to inspect deployed artifacts.');
14
- this.log('Run "swell inspect --help" for more information.');
15
+ this.log('Run "swell inspect --help" to see available subcommands.');
15
16
  }
16
17
  }
@@ -6,7 +6,9 @@ export default class Models extends SwellCommand {
6
6
  'collection-path': import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
7
7
  };
8
8
  static flags: {
9
+ app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
9
10
  live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
+ json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
12
  yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
11
13
  };
12
14
  static examples: string[];
@@ -17,10 +19,23 @@ export default class Models extends SwellCommand {
17
19
  private showAppModels;
18
20
  private showModels;
19
21
  private getModelPath;
20
- private getAppSlugId;
21
22
  private showModelDetail;
22
23
  private resolveModelPath;
23
24
  private resolveAppModelPath;
24
25
  private throwModelNotFound;
25
- private showModel;
26
+ private emitModel;
27
+ /**
28
+ * Detail-mode hints for a model record. Mirrors `InspectResourceCommand.hints`
29
+ * but lives here because `models` doesn't extend that base.
30
+ *
31
+ * Two hints: the records collection at the model's runtime address, and the
32
+ * mutation event stream filtered by canonical model string. Both interpolate
33
+ * from the record's own fields — `app_id`, `namespace`, `name` — so we stay
34
+ * consistent with column 1 if the platform renames anything.
35
+ *
36
+ * Canonical model string: `accounts` (system), `content/blogs` (namespaced
37
+ * system), `apps/<hex>/<col>` (app). The records path mirrors that with a
38
+ * leading slash.
39
+ */
40
+ private modelHints;
26
41
  }