@agentrhq/webcmd 0.4.1 → 0.4.3
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/README.md +3 -0
- package/cli-manifest.json +955 -96
- package/clis/_shared/site-auth.js +0 -1
- package/clis/_shared/site-auth.test.js +0 -1
- package/clis/amazon-in/auth.js +44 -0
- package/clis/amazon-in/checkout-status.js +54 -0
- package/clis/amazon-in/checkout.js +478 -0
- package/clis/amazon-in/parsers.js +252 -0
- package/clis/amazon-in/parsers.test.js +230 -0
- package/clis/amazon-in/product.js +72 -0
- package/clis/amazon-in/search.js +76 -0
- package/clis/amazon-in/shared.js +40 -0
- package/clis/amazon-in/wishlist.js +98 -0
- package/clis/blinkit/checkout.js +0 -1
- package/clis/blinkit/place-order.js +0 -1
- package/clis/chatgpt/model.js +2 -2
- package/clis/chatgpt/model.test.js +7 -1
- package/clis/chatgpt/utils.js +8 -0
- package/clis/chatgpt/utils.test.js +92 -1
- package/clis/district/checkout.js +0 -1
- package/clis/district/seats.js +0 -1
- package/clis/district/set-location.js +0 -1
- package/clis/district/showtimes.js +0 -1
- package/clis/google/images.js +456 -0
- package/clis/google/images.test.js +375 -0
- package/clis/instagram/user.js +5 -13
- package/clis/instagram/user.test.js +66 -0
- package/clis/mercury/check-login.js +0 -1
- package/clis/mercury/reimbursement-draft.js +0 -1
- package/clis/practo/book-confirm.js +0 -1
- package/clis/practo/cancel.js +0 -1
- package/clis/trip/attraction.js +74 -0
- package/clis/trip/car.js +74 -0
- package/clis/trip/deals.js +61 -0
- package/clis/trip/flight-round.js +88 -0
- package/clis/trip/flight.js +83 -0
- package/clis/trip/hotel-search.js +80 -0
- package/clis/trip/hotel.js +54 -0
- package/clis/trip/package.js +93 -0
- package/clis/trip/search.js +43 -0
- package/clis/trip/tour.js +84 -0
- package/clis/trip/train.js +76 -0
- package/clis/trip/transfer.js +82 -0
- package/clis/trip/trip.test.js +1420 -0
- package/clis/trip/utils.js +911 -0
- package/dist/src/build-manifest.js +0 -1
- package/dist/src/build-manifest.test.js +4 -0
- package/dist/src/cli.js +7 -7
- package/dist/src/cli.test.js +26 -10
- package/dist/src/command-presentation.js +1 -1
- package/dist/src/command-presentation.test.js +3 -0
- package/dist/src/command-surface.js +1 -1
- package/dist/src/command-surface.test.js +4 -0
- package/dist/src/commands/auth.js +0 -2
- package/dist/src/commands/auth.test.js +0 -2
- package/dist/src/discovery.js +0 -1
- package/dist/src/execution.js +3 -3
- package/dist/src/execution.test.js +68 -15
- package/dist/src/hosted/browser-args.js +2 -2
- package/dist/src/hosted/browser-args.test.js +10 -0
- package/dist/src/hosted/client.d.ts +8 -10
- package/dist/src/hosted/client.js +26 -34
- package/dist/src/hosted/client.test.js +79 -24
- package/dist/src/hosted/runner.js +7 -35
- package/dist/src/hosted/runner.test.js +27 -51
- package/dist/src/hosted/types.d.ts +1 -5
- package/dist/src/manifest-types.d.ts +0 -2
- package/dist/src/registry.d.ts +0 -2
- package/dist/src/registry.js +0 -1
- package/dist/src/root-command-surface.js +10 -2
- package/hosted-contract.json +2627 -1535
- package/package.json +2 -2
- package/skills/webcmd-browser/SKILL.md +2 -1
- package/skills/webcmd-usage/SKILL.md +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { HostedClient } from './client.js';
|
|
2
|
+
import { HostedClient, resolveWorkspace } from './client.js';
|
|
3
3
|
const invalidTraceUrlCases = [
|
|
4
4
|
{
|
|
5
5
|
name: 'raw absolute Kernel URL with token',
|
|
@@ -170,7 +170,7 @@ describe('HostedClient', () => {
|
|
|
170
170
|
const profile = {
|
|
171
171
|
id: 'profile_work',
|
|
172
172
|
name: 'Work',
|
|
173
|
-
|
|
173
|
+
workspace: 'user_64256',
|
|
174
174
|
default: false,
|
|
175
175
|
status: 'available',
|
|
176
176
|
createdAt: '2026-07-08T00:00:00.000Z',
|
|
@@ -187,34 +187,16 @@ describe('HostedClient', () => {
|
|
|
187
187
|
...(init?.body ? { body: String(init.body) } : {}),
|
|
188
188
|
});
|
|
189
189
|
const path = new URL(String(url)).pathname;
|
|
190
|
-
if (path === '/v1/profiles' && init?.method === 'POST') {
|
|
191
|
-
return new Response(JSON.stringify({ ok: true, profile }), { status: 201 });
|
|
192
|
-
}
|
|
193
190
|
if (path.startsWith('/v1/profiles/') && init?.method === 'DELETE') {
|
|
194
191
|
return new Response(JSON.stringify({ ok: true, deleted: true }));
|
|
195
192
|
}
|
|
196
|
-
if (path.startsWith('/v1/profiles/')) {
|
|
197
|
-
return new Response(JSON.stringify({ ok: true, profile }));
|
|
198
|
-
}
|
|
199
193
|
return new Response(JSON.stringify({ ok: true, profiles: [profile] }));
|
|
200
194
|
},
|
|
201
195
|
});
|
|
202
|
-
await expect(client.listProfiles({
|
|
203
|
-
.resolves.toEqual({ ok: true, profiles: [profile] });
|
|
204
|
-
await expect(client.createProfile({ name: 'Work', userId: 'user_64256' }))
|
|
205
|
-
.resolves.toMatchObject({ profile: { id: 'profile_work', status: 'available' } });
|
|
206
|
-
await expect(client.getProfile('profile_work')).resolves.toMatchObject({
|
|
207
|
-
profile: { id: 'profile_work' },
|
|
208
|
-
});
|
|
196
|
+
await expect(client.listProfiles()).resolves.toEqual({ ok: true, profiles: [profile] });
|
|
209
197
|
await expect(client.deleteProfile('profile/work')).resolves.toEqual({ ok: true, deleted: true });
|
|
210
198
|
expect(requests).toEqual([
|
|
211
|
-
{ url: 'https://api.example.com/v1/profiles
|
|
212
|
-
{
|
|
213
|
-
url: 'https://api.example.com/v1/profiles',
|
|
214
|
-
method: 'POST',
|
|
215
|
-
body: JSON.stringify({ name: 'Work', userId: 'user_64256' }),
|
|
216
|
-
},
|
|
217
|
-
{ url: 'https://api.example.com/v1/profiles/profile_work', method: 'GET' },
|
|
199
|
+
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
218
200
|
{ url: 'https://api.example.com/v1/profiles/profile%2Fwork', method: 'DELETE' },
|
|
219
201
|
]);
|
|
220
202
|
});
|
|
@@ -222,12 +204,12 @@ describe('HostedClient', () => {
|
|
|
222
204
|
{ name: 'private provider field', change: { kernelProfileId: 'private' } },
|
|
223
205
|
{ name: 'missing updatedAt', change: { updatedAt: undefined } },
|
|
224
206
|
{ name: 'non-nullable name shape', change: { name: 7 } },
|
|
225
|
-
{ name: 'non-nullable
|
|
207
|
+
{ name: 'non-nullable workspace shape', change: { workspace: false } },
|
|
226
208
|
])('rejects a hosted profile with $name', async ({ change }) => {
|
|
227
209
|
const profile = {
|
|
228
210
|
id: 'profile_work',
|
|
229
211
|
name: null,
|
|
230
|
-
|
|
212
|
+
workspace: null,
|
|
231
213
|
default: false,
|
|
232
214
|
status: 'pending',
|
|
233
215
|
createdAt: '2026-07-08T00:00:00.000Z',
|
|
@@ -850,4 +832,77 @@ describe('HostedClient', () => {
|
|
|
850
832
|
},
|
|
851
833
|
]);
|
|
852
834
|
});
|
|
835
|
+
it('attaches the workspace header when a workspace is configured', async () => {
|
|
836
|
+
const requests = [];
|
|
837
|
+
const client = new HostedClient({
|
|
838
|
+
apiBaseUrl: 'https://api.example.com',
|
|
839
|
+
apiKey: 'key',
|
|
840
|
+
workspace: resolveWorkspace([], { WEBCMD_WORKSPACE: 'user_64256' }),
|
|
841
|
+
fetchImpl: async (_url, init) => {
|
|
842
|
+
requests.push({ workspace: new Headers(init?.headers).get('x-webcmd-workspace') });
|
|
843
|
+
return new Response(JSON.stringify({
|
|
844
|
+
ok: true,
|
|
845
|
+
result: [],
|
|
846
|
+
execution: { id: 'exec_1', command: 'github/whoami', status: 'succeeded' },
|
|
847
|
+
}), { status: 200 });
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
await client.execute({ command: 'github/whoami', args: {} });
|
|
851
|
+
expect(requests).toEqual([{ workspace: 'user_64256' }]);
|
|
852
|
+
});
|
|
853
|
+
it('attaches the workspace header for the --workspace=<value> equals form', async () => {
|
|
854
|
+
const requests = [];
|
|
855
|
+
const client = new HostedClient({
|
|
856
|
+
apiBaseUrl: 'https://api.example.com',
|
|
857
|
+
apiKey: 'key',
|
|
858
|
+
workspace: resolveWorkspace(['--workspace=user_64256'], {}),
|
|
859
|
+
fetchImpl: async (_url, init) => {
|
|
860
|
+
requests.push({ workspace: new Headers(init?.headers).get('x-webcmd-workspace') });
|
|
861
|
+
return new Response(JSON.stringify({
|
|
862
|
+
ok: true,
|
|
863
|
+
result: [],
|
|
864
|
+
execution: { id: 'exec_1', command: 'github/whoami', status: 'succeeded' },
|
|
865
|
+
}), { status: 200 });
|
|
866
|
+
},
|
|
867
|
+
});
|
|
868
|
+
await client.execute({ command: 'github/whoami', args: {} });
|
|
869
|
+
expect(requests).toEqual([{ workspace: 'user_64256' }]);
|
|
870
|
+
});
|
|
871
|
+
it('omits the workspace header when no workspace is configured', async () => {
|
|
872
|
+
const requests = [];
|
|
873
|
+
const client = new HostedClient({
|
|
874
|
+
apiBaseUrl: 'https://api.example.com',
|
|
875
|
+
apiKey: 'key',
|
|
876
|
+
fetchImpl: async (_url, init) => {
|
|
877
|
+
requests.push({ workspace: new Headers(init?.headers).get('x-webcmd-workspace') });
|
|
878
|
+
return new Response(JSON.stringify({
|
|
879
|
+
ok: true,
|
|
880
|
+
result: [],
|
|
881
|
+
execution: { id: 'exec_1', command: 'github/whoami', status: 'succeeded' },
|
|
882
|
+
}), { status: 200 });
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
await client.execute({ command: 'github/whoami', args: {} });
|
|
886
|
+
expect(requests).toEqual([{ workspace: null }]);
|
|
887
|
+
});
|
|
888
|
+
});
|
|
889
|
+
describe('resolveWorkspace', () => {
|
|
890
|
+
it('returns the --workspace flag value when present', () => {
|
|
891
|
+
expect(resolveWorkspace(['--workspace', 'flagws'], {})).toBe('flagws');
|
|
892
|
+
});
|
|
893
|
+
it('returns the --workspace=<value> flag value when present', () => {
|
|
894
|
+
expect(resolveWorkspace(['--workspace=user_64256'], {})).toBe('user_64256');
|
|
895
|
+
});
|
|
896
|
+
it('--workspace overrides the env var', () => {
|
|
897
|
+
expect(resolveWorkspace(['--workspace', 'flagws'], { WEBCMD_WORKSPACE: 'envws' })).toBe('flagws');
|
|
898
|
+
});
|
|
899
|
+
it('falls back to WEBCMD_WORKSPACE when no flag is present', () => {
|
|
900
|
+
expect(resolveWorkspace([], { WEBCMD_WORKSPACE: 'envws' })).toBe('envws');
|
|
901
|
+
});
|
|
902
|
+
it('trims WEBCMD_WORKSPACE', () => {
|
|
903
|
+
expect(resolveWorkspace([], { WEBCMD_WORKSPACE: ' envws ' })).toBe('envws');
|
|
904
|
+
});
|
|
905
|
+
it('returns undefined when neither --workspace nor WEBCMD_WORKSPACE is set', () => {
|
|
906
|
+
expect(resolveWorkspace([], {})).toBeUndefined();
|
|
907
|
+
});
|
|
853
908
|
});
|
|
@@ -15,7 +15,7 @@ import { StreamWriteError, writeToStream } from '../stream-write.js';
|
|
|
15
15
|
import { PKG_VERSION } from '../version.js';
|
|
16
16
|
import { getCompletionScriptFast } from '../completion-fast.js';
|
|
17
17
|
import { browserCommandCatalog } from '../browser/command-catalog.js';
|
|
18
|
-
import { HostedClient, HostedClientError } from './client.js';
|
|
18
|
+
import { HostedClient, HostedClientError, resolveWorkspace } from './client.js';
|
|
19
19
|
import { parseHostedInvocation } from './args.js';
|
|
20
20
|
import { HostedBrowserHelp, parseHostedBrowserStructure } from './browser-args.js';
|
|
21
21
|
import { materializeHostedOutputs, prepareHostedFiles, rewriteHostedOutputResultPaths } from './files.js';
|
|
@@ -53,6 +53,7 @@ export async function runHostedCli(argv, opts = {}) {
|
|
|
53
53
|
const client = new HostedClient({
|
|
54
54
|
apiBaseUrl: config.hosted.apiBaseUrl,
|
|
55
55
|
apiKey: credential.apiKey,
|
|
56
|
+
workspace: resolveWorkspace(argv, opts.env ?? process.env),
|
|
56
57
|
fetchImpl: opts.fetchImpl,
|
|
57
58
|
});
|
|
58
59
|
await dispatchHosted(argv, client, stdout, stderr, opts.now ?? Date.now);
|
|
@@ -148,7 +149,7 @@ async function dispatchHosted(argv, client, stdout, stderr, now) {
|
|
|
148
149
|
}
|
|
149
150
|
if (args[0] === 'profile') {
|
|
150
151
|
if (args[1] === 'rename' || args[1] === 'use') {
|
|
151
|
-
throw new ConfigError(`webcmd profile ${args[1]} is not available in hosted mode.`, 'Hosted mode supports: webcmd profile list
|
|
152
|
+
throw new ConfigError(`webcmd profile ${args[1]} is not available in hosted mode.`, 'Hosted mode supports: webcmd profile list and delete.');
|
|
152
153
|
}
|
|
153
154
|
const parsed = parseHostedProfileSurface(args.slice(1), normalized.literal);
|
|
154
155
|
if (parsed.kind === 'help') {
|
|
@@ -577,7 +578,7 @@ function parseHostedProfileSurface(argv, literal) {
|
|
|
577
578
|
root.exitOverride().configureOutput(output);
|
|
578
579
|
profile.exitOverride().configureOutput(output);
|
|
579
580
|
const configureFormat = (command) => command.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table');
|
|
580
|
-
const setParsed = (command, surface, value
|
|
581
|
+
const setParsed = (command, surface, value) => {
|
|
581
582
|
const options = surface.opts();
|
|
582
583
|
parsed = {
|
|
583
584
|
kind: 'run',
|
|
@@ -585,15 +586,10 @@ function parseHostedProfileSurface(argv, literal) {
|
|
|
585
586
|
format: options.format,
|
|
586
587
|
formatExplicit: surface.getOptionValueSource('format') === 'cli',
|
|
587
588
|
...(value !== undefined ? { value } : {}),
|
|
588
|
-
...(userId !== undefined ? { userId } : {}),
|
|
589
589
|
};
|
|
590
590
|
};
|
|
591
591
|
const list = configureFormat(profile.command('list'));
|
|
592
592
|
list.exitOverride().configureOutput(output).action(() => setParsed('list', list));
|
|
593
|
-
const create = configureFormat(profile.command('create').argument('<name>').option('--user-id <id>'));
|
|
594
|
-
create.exitOverride().configureOutput(output).action((name, options) => setParsed('create', create, name, options.userId));
|
|
595
|
-
const get = configureFormat(profile.command('get').argument('<profile>'));
|
|
596
|
-
get.exitOverride().configureOutput(output).action((selector) => setParsed('get', get, selector));
|
|
597
593
|
const remove = configureFormat(profile.command('delete').argument('<profile-id>'));
|
|
598
594
|
remove.exitOverride().configureOutput(output).action((profileId) => setParsed('delete', remove, profileId));
|
|
599
595
|
try {
|
|
@@ -612,39 +608,15 @@ function parseHostedProfileSurface(argv, literal) {
|
|
|
612
608
|
return parsed;
|
|
613
609
|
}
|
|
614
610
|
async function dispatchHostedProfile(parsed, client, stdout) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
else if (parsed.command === 'create') {
|
|
620
|
-
result = (await client.createProfile({
|
|
621
|
-
name: parsed.value,
|
|
622
|
-
...(parsed.userId !== undefined ? { userId: parsed.userId } : {}),
|
|
623
|
-
})).profile;
|
|
624
|
-
}
|
|
625
|
-
else if (parsed.command === 'get') {
|
|
626
|
-
result = resolveHostedProfile((await client.listProfiles()).profiles, parsed.value);
|
|
627
|
-
}
|
|
628
|
-
else {
|
|
629
|
-
result = await client.deleteProfile(parsed.value);
|
|
630
|
-
}
|
|
611
|
+
const result = parsed.command === 'list'
|
|
612
|
+
? (await client.listProfiles()).profiles
|
|
613
|
+
: await client.deleteProfile(parsed.value);
|
|
631
614
|
await renderOutput(result, {
|
|
632
615
|
fmt: parsed.format,
|
|
633
616
|
fmtExplicit: parsed.formatExplicit,
|
|
634
617
|
stdout,
|
|
635
618
|
});
|
|
636
619
|
}
|
|
637
|
-
function resolveHostedProfile(profiles, selector) {
|
|
638
|
-
const matches = profiles.filter(profile => profile.id === selector || profile.name === selector || profile.userId === selector);
|
|
639
|
-
const distinct = [...new Map(matches.map(profile => [profile.id, profile])).values()];
|
|
640
|
-
if (distinct.length === 0) {
|
|
641
|
-
throw new CliError('PROFILE_NOT_FOUND', 'Profile was not found.', undefined, EXIT_CODES.EMPTY_RESULT);
|
|
642
|
-
}
|
|
643
|
-
if (distinct.length > 1) {
|
|
644
|
-
throw new CliError('AMBIGUOUS_PROFILE', 'The profile selector matched multiple profiles.', 'Use the immutable profile ID returned by webcmd profile list.', EXIT_CODES.TEMPFAIL);
|
|
645
|
-
}
|
|
646
|
-
return distinct[0];
|
|
647
|
-
}
|
|
648
620
|
function parseHostedCompletionSurface(argv, literal) {
|
|
649
621
|
let stdout = '';
|
|
650
622
|
let stderr = '';
|
|
@@ -226,14 +226,14 @@ describe('runHostedCli', () => {
|
|
|
226
226
|
const publicProfile = {
|
|
227
227
|
id: 'profile_work',
|
|
228
228
|
name: 'Work',
|
|
229
|
-
|
|
229
|
+
workspace: 'ws_demo',
|
|
230
230
|
default: false,
|
|
231
231
|
status: 'available',
|
|
232
232
|
createdAt: '2026-07-24T00:00:00.000Z',
|
|
233
233
|
updatedAt: '2026-07-24T00:00:00.000Z',
|
|
234
234
|
lastUsedAt: '2026-07-24T00:00:00.000Z',
|
|
235
235
|
};
|
|
236
|
-
it('lists
|
|
236
|
+
it('lists and deletes hosted profiles without fetching the manifest', async () => {
|
|
237
237
|
const requests = [];
|
|
238
238
|
const fetchImpl = vi.fn(async (url, init) => {
|
|
239
239
|
const request = {
|
|
@@ -242,9 +242,6 @@ describe('runHostedCli', () => {
|
|
|
242
242
|
...(init?.body ? { body: JSON.parse(String(init.body)) } : {}),
|
|
243
243
|
};
|
|
244
244
|
requests.push(request);
|
|
245
|
-
if (request.method === 'POST') {
|
|
246
|
-
return new Response(JSON.stringify({ ok: true, profile: publicProfile }), { status: 201 });
|
|
247
|
-
}
|
|
248
245
|
if (request.method === 'DELETE') {
|
|
249
246
|
return new Response(JSON.stringify({ ok: true, deleted: true }));
|
|
250
247
|
}
|
|
@@ -252,10 +249,6 @@ describe('runHostedCli', () => {
|
|
|
252
249
|
});
|
|
253
250
|
for (const argv of [
|
|
254
251
|
['profile', 'list', '-f', 'json'],
|
|
255
|
-
['profile', 'create', 'Work', '--user-id', 'user_64256', '-f', 'json'],
|
|
256
|
-
['profile', 'get', 'Work', '-f', 'json'],
|
|
257
|
-
['profile', 'get', 'user_64256', '-f', 'json'],
|
|
258
|
-
['profile', 'get', 'profile_work', '-f', 'json'],
|
|
259
252
|
['profile', 'delete', 'profile_work', '-f', 'json'],
|
|
260
253
|
]) {
|
|
261
254
|
const stdout = sink();
|
|
@@ -271,63 +264,46 @@ describe('runHostedCli', () => {
|
|
|
271
264
|
expect(stdout.text()).toContain(argv[1] === 'delete' ? '"deleted": true' : '"id": "profile_work"');
|
|
272
265
|
}
|
|
273
266
|
expect(requests).toEqual([
|
|
274
|
-
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
275
|
-
{
|
|
276
|
-
url: 'https://api.example.com/v1/profiles',
|
|
277
|
-
method: 'POST',
|
|
278
|
-
body: { name: 'Work', userId: 'user_64256' },
|
|
279
|
-
},
|
|
280
|
-
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
281
|
-
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
282
267
|
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
283
268
|
{ url: 'https://api.example.com/v1/profiles/profile_work', method: 'DELETE' },
|
|
284
269
|
]);
|
|
285
270
|
expect(requests.some(request => request.url.endsWith('/v1/manifest'))).toBe(false);
|
|
286
271
|
});
|
|
287
|
-
it('
|
|
272
|
+
it.each(['create', 'get'])('rejects the removed profile %s subcommand', async (command) => {
|
|
273
|
+
const stderr = sink();
|
|
274
|
+
const fetchImpl = vi.fn();
|
|
275
|
+
const result = await runHostedCli(['profile', command, 'Work'], {
|
|
276
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
277
|
+
stderr: stderr.stream,
|
|
278
|
+
fetchImpl,
|
|
279
|
+
});
|
|
280
|
+
expect(result.handled).toBe(true);
|
|
281
|
+
expect(stderr.text()).toMatch(/unknown command|not supported/i);
|
|
282
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
it('threads the ambient workspace onto hosted profile requests', async () => {
|
|
288
285
|
const cases = [
|
|
289
|
-
{
|
|
290
|
-
|
|
291
|
-
profiles: [{ ...publicProfile, id: 'profile_same', name: 'same', userId: 'same' }],
|
|
292
|
-
exitCode: 0,
|
|
293
|
-
stdout: '"id": "profile_same"',
|
|
294
|
-
stderr: '',
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
selector: 'missing',
|
|
298
|
-
profiles: [publicProfile],
|
|
299
|
-
exitCode: 66,
|
|
300
|
-
stdout: '',
|
|
301
|
-
stderr: 'code: PROFILE_NOT_FOUND',
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
selector: 'shared',
|
|
305
|
-
profiles: [
|
|
306
|
-
{ ...publicProfile, id: 'profile_one', name: 'shared' },
|
|
307
|
-
{ ...publicProfile, id: 'profile_two', name: 'Other', userId: 'shared' },
|
|
308
|
-
],
|
|
309
|
-
exitCode: 75,
|
|
310
|
-
stdout: '',
|
|
311
|
-
stderr: 'code: AMBIGUOUS_PROFILE',
|
|
312
|
-
},
|
|
286
|
+
{ name: 'from WEBCMD_WORKSPACE env', argv: ['profile', 'list', '-f', 'json'], env: { WEBCMD_WORKSPACE: 'ws1' }, expected: 'ws1' },
|
|
287
|
+
{ name: 'from --workspace flag', argv: ['--workspace', 'ws2', 'profile', 'list', '-f', 'json'], env: {}, expected: 'ws2' },
|
|
313
288
|
];
|
|
314
289
|
for (const testCase of cases) {
|
|
290
|
+
let capturedHeader = null;
|
|
291
|
+
const fetchImpl = vi.fn(async (_url, init) => {
|
|
292
|
+
capturedHeader = new Headers(init?.headers).get('x-webcmd-workspace');
|
|
293
|
+
return new Response(JSON.stringify({ ok: true, profiles: [publicProfile] }));
|
|
294
|
+
});
|
|
315
295
|
const stdout = sink();
|
|
316
296
|
const stderr = sink();
|
|
317
|
-
const
|
|
318
|
-
const result = await runHostedCli(['profile', 'get', testCase.selector, '-f', 'json'], {
|
|
297
|
+
const result = await runHostedCli(testCase.argv, {
|
|
319
298
|
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
320
299
|
stdout: stdout.stream,
|
|
321
300
|
stderr: stderr.stream,
|
|
322
301
|
fetchImpl,
|
|
302
|
+
env: testCase.env,
|
|
323
303
|
});
|
|
324
|
-
expect(result
|
|
325
|
-
expect(
|
|
326
|
-
expect(
|
|
327
|
-
if (testCase.selector === 'shared') {
|
|
328
|
-
expect(stderr.text()).toContain('Use the immutable profile ID returned by webcmd profile list.');
|
|
329
|
-
}
|
|
330
|
-
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
305
|
+
expect(stderr.text()).toBe('');
|
|
306
|
+
expect(capturedHeader).toBe(testCase.expected);
|
|
331
307
|
}
|
|
332
308
|
});
|
|
333
309
|
it.each(['rename', 'use'])('rejects local-only profile %s in hosted mode without an API call', async (command) => {
|
|
@@ -42,7 +42,7 @@ export interface HostedManifest {
|
|
|
42
42
|
export interface HostedPublicProfile {
|
|
43
43
|
id: string;
|
|
44
44
|
name: string | null;
|
|
45
|
-
|
|
45
|
+
workspace: string | null;
|
|
46
46
|
default: boolean;
|
|
47
47
|
status: 'pending' | 'available';
|
|
48
48
|
createdAt: string;
|
|
@@ -53,10 +53,6 @@ export interface HostedProfilesResponse {
|
|
|
53
53
|
ok: true;
|
|
54
54
|
profiles: HostedPublicProfile[];
|
|
55
55
|
}
|
|
56
|
-
export interface HostedProfileResponse {
|
|
57
|
-
ok: true;
|
|
58
|
-
profile: HostedPublicProfile;
|
|
59
|
-
}
|
|
60
56
|
export interface HostedExecution {
|
|
61
57
|
id: string;
|
|
62
58
|
command: string;
|
|
@@ -42,6 +42,4 @@ export interface ManifestEntry {
|
|
|
42
42
|
siteSession?: 'ephemeral' | 'persistent';
|
|
43
43
|
/** Fresh page behavior for persistent site sessions — see CliCommand.freshPage */
|
|
44
44
|
freshPage?: boolean;
|
|
45
|
-
/** Default browser window visibility — see CliCommand.defaultWindowMode */
|
|
46
|
-
defaultWindowMode?: 'foreground' | 'background';
|
|
47
45
|
}
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -75,8 +75,6 @@ interface BaseCliCommand {
|
|
|
75
75
|
* combining them is a contradiction and fails at registration.
|
|
76
76
|
*/
|
|
77
77
|
freshPage?: boolean;
|
|
78
|
-
/** Default browser window mode for commands whose UX requires visibility. */
|
|
79
|
-
defaultWindowMode?: 'foreground' | 'background';
|
|
80
78
|
/** Override the default CLI output format when the user does not pass -f/--format. */
|
|
81
79
|
defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
|
|
82
80
|
/** Optional auth-status metadata attached by shared auth adapters. */
|
package/dist/src/registry.js
CHANGED
|
@@ -35,6 +35,11 @@ export function parseHostedRootCommandSurface(argv) {
|
|
|
35
35
|
let stderr = '';
|
|
36
36
|
const boundary = findRootCommandBoundary(input);
|
|
37
37
|
const root = configureRootCommandSurface(new Command('webcmd'))
|
|
38
|
+
// Hosted-only: registered here (not in the shared configureRootCommandSurface)
|
|
39
|
+
// so the local CLI surface is unaffected. Lets Commander's structural parse
|
|
40
|
+
// consume `--workspace <id>` before the site/command token instead of
|
|
41
|
+
// throwing an unknown-option error.
|
|
42
|
+
.option('--workspace <id>', 'Hosted workspace id/slug for the request')
|
|
38
43
|
.exitOverride()
|
|
39
44
|
.configureOutput({
|
|
40
45
|
writeOut: value => { stdout += value; },
|
|
@@ -89,13 +94,16 @@ export function parseHostedRootCommandSurface(argv) {
|
|
|
89
94
|
function findRootCommandBoundary(argv) {
|
|
90
95
|
for (let index = 0; index < argv.length; index += 1) {
|
|
91
96
|
const token = argv[index];
|
|
92
|
-
if (token === '--profile') {
|
|
97
|
+
if (token === '--profile' || token === '--workspace') {
|
|
93
98
|
// Commander requires and consumes the next token even when it is `--` or
|
|
94
99
|
// starts with a dash. Structural failures have already been reported.
|
|
100
|
+
// `--workspace` is hosted-only (not a registered Commander option here)
|
|
101
|
+
// but must still be skipped as a value pair so it and its value are not
|
|
102
|
+
// mistaken for the site/command token and forwarded to the server.
|
|
95
103
|
index += 1;
|
|
96
104
|
continue;
|
|
97
105
|
}
|
|
98
|
-
if (token.startsWith('--profile='))
|
|
106
|
+
if (token.startsWith('--profile=') || token.startsWith('--workspace='))
|
|
99
107
|
continue;
|
|
100
108
|
if (token === '--')
|
|
101
109
|
return { separatorIndex: index };
|