@swell/cli 2.7.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/app/dev.d.ts +2 -0
- package/dist/commands/app/dev.js +16 -0
- package/dist/commands/app/frontend/dev.d.ts +1 -0
- package/dist/commands/inspect/extensions.d.ts +49 -0
- package/dist/commands/inspect/extensions.js +424 -0
- package/dist/commands/logs.js +1 -1
- package/dist/create-app-command.js +14 -6
- package/dist/lib/apps/app-config.d.ts +1 -0
- package/dist/lib/apps/index.d.ts +1 -0
- package/dist/lib/inspect/extensions.d.ts +267 -0
- package/dist/lib/inspect/extensions.js +690 -0
- package/dist/lib/swell-function-wrapper.d.ts +32 -1
- package/dist/lib/swell-function-wrapper.js +74 -20
- package/oclif.manifest.json +88 -8
- package/package.json +3 -3
|
@@ -8,6 +8,7 @@ export default class AppDev extends PushAppCommand {
|
|
|
8
8
|
'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
9
9
|
port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
10
|
'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
|
+
function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
11
12
|
'storefront-id': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
13
|
'storefront-select': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
13
14
|
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
@@ -17,6 +18,7 @@ export default class AppDev extends PushAppCommand {
|
|
|
17
18
|
};
|
|
18
19
|
static summary: string;
|
|
19
20
|
functionErrors: Map<string, string>;
|
|
21
|
+
functionFilter: string | null;
|
|
20
22
|
functionPorts: Map<string, number>;
|
|
21
23
|
functionProcesses: Map<string, ChildProcess>;
|
|
22
24
|
tmpDir: string;
|
package/dist/commands/app/dev.js
CHANGED
|
@@ -18,6 +18,7 @@ export default class AppDev extends PushAppCommand {
|
|
|
18
18
|
'swell app dev --storefront-id <id>',
|
|
19
19
|
'swell app dev --port 3000',
|
|
20
20
|
'swell app dev --port 3000 --frontend-port 4000',
|
|
21
|
+
'swell app dev --function my-function',
|
|
21
22
|
];
|
|
22
23
|
static flags = {
|
|
23
24
|
'no-push': Flags.boolean({
|
|
@@ -30,6 +31,9 @@ export default class AppDev extends PushAppCommand {
|
|
|
30
31
|
'frontend-port': Flags.integer({
|
|
31
32
|
description: 'specify the port for the frontend dev server when running with frontend',
|
|
32
33
|
}),
|
|
34
|
+
function: Flags.string({
|
|
35
|
+
description: 'run only the named function',
|
|
36
|
+
}),
|
|
33
37
|
'storefront-id': Flags.string({
|
|
34
38
|
description: 'for storefront apps, identify a storefront to preview and push theme files to',
|
|
35
39
|
}),
|
|
@@ -48,6 +52,8 @@ export default class AppDev extends PushAppCommand {
|
|
|
48
52
|
};
|
|
49
53
|
static summary = `Run an app in dev mode from your local machine.`;
|
|
50
54
|
functionErrors = new Map();
|
|
55
|
+
// When set, only this function name is bundled, started, and watched
|
|
56
|
+
functionFilter = null;
|
|
51
57
|
// All available functions
|
|
52
58
|
functionPorts = new Map();
|
|
53
59
|
// Wrangler child processes to kill on cleanup
|
|
@@ -62,6 +68,7 @@ export default class AppDev extends PushAppCommand {
|
|
|
62
68
|
const { flags } = await this.parse(AppDev);
|
|
63
69
|
const { port, 'frontend-port': frontendPort } = flags;
|
|
64
70
|
const noPush = flags['no-push'];
|
|
71
|
+
this.functionFilter = flags.function ?? null;
|
|
65
72
|
if (!(await this.ensureAppExists(undefined, false))) {
|
|
66
73
|
return;
|
|
67
74
|
}
|
|
@@ -213,12 +220,18 @@ ENVIRONMENT = "development"
|
|
|
213
220
|
if (!config.isRootFunction()) {
|
|
214
221
|
continue; // Skip if not a root function config
|
|
215
222
|
}
|
|
223
|
+
if (this.functionFilter && config.name !== this.functionFilter) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
216
226
|
functions.push(config);
|
|
217
227
|
}
|
|
218
228
|
}
|
|
219
229
|
catch {
|
|
220
230
|
// functions directory doesn't exist
|
|
221
231
|
}
|
|
232
|
+
if (this.functionFilter && functions.length === 0) {
|
|
233
|
+
this.error(`Function '${this.functionFilter}' not found in ${style.path('functions/')}.`);
|
|
234
|
+
}
|
|
222
235
|
return functions;
|
|
223
236
|
}
|
|
224
237
|
async logAllFunctions(functions) {
|
|
@@ -293,6 +306,9 @@ ENVIRONMENT = "development"
|
|
|
293
306
|
if (appConfig?.type !== ConfigType.FUNCTION) {
|
|
294
307
|
return;
|
|
295
308
|
}
|
|
309
|
+
if (this.functionFilter && appConfig.name !== this.functionFilter) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
296
312
|
try {
|
|
297
313
|
const fullPath = path.join(this.appPath, appConfig.filePath);
|
|
298
314
|
// Re-bundle the function
|
|
@@ -10,6 +10,7 @@ export default class AppFrontendDev extends PushAppCommand {
|
|
|
10
10
|
'no-push': import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
11
|
port: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
12
12
|
'frontend-port': import("@oclif/core/lib/interfaces/parser.js").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
|
+
function: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
13
14
|
};
|
|
14
15
|
static orientation: {
|
|
15
16
|
env: string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { SwellCommand } from '../../swell-command.js';
|
|
2
|
+
export default class Extensions extends SwellCommand {
|
|
3
|
+
static summary: string;
|
|
4
|
+
static description: string;
|
|
5
|
+
static args: {
|
|
6
|
+
identifier: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
|
|
7
|
+
};
|
|
8
|
+
static flags: {
|
|
9
|
+
app: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
10
|
+
live: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
11
|
+
json: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
12
|
+
yes: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
|
|
13
|
+
};
|
|
14
|
+
static examples: string[];
|
|
15
|
+
run(): Promise<void>;
|
|
16
|
+
protected catch(error: Error): Promise<any>;
|
|
17
|
+
/**
|
|
18
|
+
* List mode is bounded to two parallel round-trip phases regardless of app
|
|
19
|
+
* count: phase 1 fans `/client/apps`, the three settings singletons, and a
|
|
20
|
+
* single `/data/:functions` filter; phase 2 fans `/apps/<id>/configs?type=component`
|
|
21
|
+
* only for apps with a non-empty `extensions[]`.
|
|
22
|
+
*
|
|
23
|
+
* `include` / `aggregate` are NOT used here — the CLI's `node-fetch`
|
|
24
|
+
* transport cannot transmit GET-with-body, and URL-form transmission of
|
|
25
|
+
* those payloads is silently dropped server-side. Verified empirically
|
|
26
|
+
* against the live admin API. Don't reach for those primitives.
|
|
27
|
+
*/
|
|
28
|
+
private showList;
|
|
29
|
+
/**
|
|
30
|
+
* Detail mode is bounded to one round-trip's worth of latency: the install
|
|
31
|
+
* record (or `/apps/<id>` fallback), the relevant settings singleton(s),
|
|
32
|
+
* `/data/:functions?where[app_id]=<id>&where[extension]=<extId>`, and
|
|
33
|
+
* `/apps/<id>/configs?type=component` — all in parallel.
|
|
34
|
+
*/
|
|
35
|
+
private showDetail;
|
|
36
|
+
/**
|
|
37
|
+
* `extensions` emits a synthesized envelope rather than a raw API record —
|
|
38
|
+
* other inspect subcommands print one upstream record, but here the JSON
|
|
39
|
+
* nests `manifest`, `native_bindings[].record`, `bound.functions[]`, and
|
|
40
|
+
* `bound.components[]` under shared status fields.
|
|
41
|
+
*
|
|
42
|
+
* The `Next steps:` footer mixes runnable shell commands and merchant-UI
|
|
43
|
+
* lines (prefixed `(merchant)`). The structured `action` and `action_owner`
|
|
44
|
+
* fields above give an agent a parseable signal independent of the footer.
|
|
45
|
+
*/
|
|
46
|
+
private emitDetail;
|
|
47
|
+
private fetchSafe;
|
|
48
|
+
private printPreamble;
|
|
49
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -175,7 +175,7 @@ export default class Logs extends SwellCommand {
|
|
|
175
175
|
type: Flags.string({
|
|
176
176
|
description: 'filter logs by type',
|
|
177
177
|
multiple: true,
|
|
178
|
-
options: ['api', 'function', 'webhook'],
|
|
178
|
+
options: ['api', 'function', 'webhook', 'transaction'],
|
|
179
179
|
}),
|
|
180
180
|
};
|
|
181
181
|
static summary = 'Output or stream store logs to the terminal.';
|
|
@@ -310,9 +310,13 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
310
310
|
); */
|
|
311
311
|
}
|
|
312
312
|
catch (error) {
|
|
313
|
-
spinner.fail(`Error creating ${projectType?.name} app
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
spinner.fail(`Error creating ${projectType?.name} app:`);
|
|
314
|
+
const detail = [error.stdout, error.stderr]
|
|
315
|
+
.map((s) => (s || '').trim())
|
|
316
|
+
.filter(Boolean)
|
|
317
|
+
.join('\n') || (error.message ?? '').trim();
|
|
318
|
+
if (detail)
|
|
319
|
+
this.log(detail);
|
|
316
320
|
return false;
|
|
317
321
|
}
|
|
318
322
|
// Ensure frontend package.json has correct name for workspace
|
|
@@ -383,9 +387,13 @@ export class CreateAppCommand extends SwellCommand {
|
|
|
383
387
|
}
|
|
384
388
|
}
|
|
385
389
|
catch (error) {
|
|
386
|
-
spinner.fail(`Error creating theme template
|
|
387
|
-
|
|
388
|
-
|
|
390
|
+
spinner.fail(`Error creating theme template:`);
|
|
391
|
+
const detail = [error.stdout, error.stderr]
|
|
392
|
+
.map((s) => (s || '').trim())
|
|
393
|
+
.filter(Boolean)
|
|
394
|
+
.join('\n') || (error.message ?? '').trim();
|
|
395
|
+
if (detail)
|
|
396
|
+
this.log(detail);
|
|
389
397
|
return false;
|
|
390
398
|
}
|
|
391
399
|
spinner.succeed(`Theme template initialized in ${configPath}/theme/`);
|
package/dist/lib/apps/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
2
3
|
import { AppConfig } from './app-config.js';
|
|
3
4
|
export { AppConfig, FunctionProcessingError, IgnoringFileError, } from './app-config.js';
|
|
4
5
|
export { allBaseFilesInDir, allConfigDirsPaths, allConfigFilesInDir, allConfigFilesPaths, allConfigFilesPathsByType, getAllConfigPaths, globAllFilesByPath, isPathDirectory, } from './paths.js';
|