@agentrhq/webcmd 0.2.4 → 0.3.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.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/clis/_shared/site-auth.js +3 -3
  3. package/clis/_shared/site-auth.test.js +4 -4
  4. package/clis/slock/whoami.test.js +2 -2
  5. package/dist/src/browser/daemon-client.d.ts +2 -1
  6. package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
  7. package/dist/src/browser/runtime/local-cloak/provider.test.js +36 -0
  8. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +3 -2
  9. package/dist/src/browser/runtime/local-cloak/session-manager.js +4 -2
  10. package/dist/src/cli.js +5 -4
  11. package/dist/src/cli.test.js +11 -5
  12. package/dist/src/commands/auth.js +3 -2
  13. package/dist/src/commands/auth.test.js +6 -6
  14. package/dist/src/generate-release-notes-cli.test.js +5 -0
  15. package/dist/src/hosted/args.d.ts +9 -0
  16. package/dist/src/hosted/args.js +101 -0
  17. package/dist/src/hosted/args.test.d.ts +1 -0
  18. package/dist/src/hosted/args.test.js +35 -0
  19. package/dist/src/hosted/client.d.ts +30 -0
  20. package/dist/src/hosted/client.js +122 -0
  21. package/dist/src/hosted/client.test.d.ts +1 -0
  22. package/dist/src/hosted/client.test.js +119 -0
  23. package/dist/src/hosted/config.d.ts +50 -0
  24. package/dist/src/hosted/config.js +90 -0
  25. package/dist/src/hosted/config.test.d.ts +1 -0
  26. package/dist/src/hosted/config.test.js +48 -0
  27. package/dist/src/hosted/manifest.d.ts +9 -0
  28. package/dist/src/hosted/manifest.js +92 -0
  29. package/dist/src/hosted/manifest.test.d.ts +1 -0
  30. package/dist/src/hosted/manifest.test.js +46 -0
  31. package/dist/src/hosted/runner.d.ts +12 -0
  32. package/dist/src/hosted/runner.js +404 -0
  33. package/dist/src/hosted/runner.test.d.ts +1 -0
  34. package/dist/src/hosted/runner.test.js +189 -0
  35. package/dist/src/hosted/setup.d.ts +9 -0
  36. package/dist/src/hosted/setup.js +49 -0
  37. package/dist/src/hosted/setup.test.d.ts +1 -0
  38. package/dist/src/hosted/setup.test.js +68 -0
  39. package/dist/src/hosted/types.d.ts +97 -0
  40. package/dist/src/hosted/types.js +1 -0
  41. package/dist/src/main.js +14 -0
  42. package/dist/src/release-notes.d.ts +6 -1
  43. package/dist/src/release-notes.js +184 -4
  44. package/dist/src/release-notes.test.js +128 -1
  45. package/package.json +1 -1
  46. package/scripts/generate-release-notes.ts +1 -1
  47. package/skills/smart-search/SKILL.md +14 -3
  48. package/skills/webcmd-adapter-author/SKILL.md +2 -2
  49. package/skills/webcmd-adapter-author/references/adapter-template.md +25 -3
  50. package/skills/webcmd-autofix/SKILL.md +1 -1
  51. package/skills/webcmd-usage/SKILL.md +22 -9
  52. package/clis/antigravity/SKILL.md +0 -38
@@ -0,0 +1,122 @@
1
+ import { CliError, EXIT_CODES } from '../errors.js';
2
+ export class HostedClientError extends CliError {
3
+ constructor(code, message, help, exitCode = EXIT_CODES.GENERIC_ERROR) {
4
+ super(code, message, help, exitCode);
5
+ }
6
+ }
7
+ export class HostedClient {
8
+ apiBaseUrl;
9
+ apiKey;
10
+ fetchImpl;
11
+ constructor(options) {
12
+ this.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, '');
13
+ this.apiKey = options.apiKey;
14
+ this.fetchImpl = options.fetchImpl ?? fetch;
15
+ }
16
+ async getMe() {
17
+ return this.request('/v1/me');
18
+ }
19
+ async getManifest() {
20
+ const body = await this.request('/v1/manifest');
21
+ if (!body.manifest || !Array.isArray(body.manifest.commands)) {
22
+ throw new HostedClientError('HOSTED_PROTOCOL', 'Webcmd Cloud returned an invalid manifest.');
23
+ }
24
+ return body.manifest;
25
+ }
26
+ async execute(input) {
27
+ return this.request('/v1/execute', {
28
+ method: 'POST',
29
+ body: JSON.stringify(input),
30
+ });
31
+ }
32
+ async startBrowserRun(session, input) {
33
+ return this.request(`/v1/browser/${encodeURIComponent(session)}/runs`, {
34
+ method: 'POST',
35
+ body: JSON.stringify(input),
36
+ });
37
+ }
38
+ async browserAction(session, executionId, input) {
39
+ return this.request(`/v1/browser/${encodeURIComponent(session)}/runs/${encodeURIComponent(executionId)}/actions`, {
40
+ method: 'POST',
41
+ body: JSON.stringify(input),
42
+ });
43
+ }
44
+ async finishBrowserRun(session, executionId, input) {
45
+ return this.request(`/v1/browser/${encodeURIComponent(session)}/runs/${encodeURIComponent(executionId)}/finish`, {
46
+ method: 'POST',
47
+ body: JSON.stringify(input),
48
+ });
49
+ }
50
+ async runBrowserAction(session, input) {
51
+ const { command, trace, windowMode, action, profile, args } = input;
52
+ const run = await this.startBrowserRun(session, {
53
+ command,
54
+ args,
55
+ ...(profile !== undefined ? { profile } : {}),
56
+ ...(windowMode !== undefined ? { windowMode } : {}),
57
+ ...(trace !== undefined ? { trace } : {}),
58
+ });
59
+ try {
60
+ const actionResponse = await this.browserAction(session, run.run.executionId, {
61
+ action,
62
+ args,
63
+ ...(profile !== undefined ? { profile } : {}),
64
+ });
65
+ const finished = await this.finishBrowserRun(session, run.run.executionId, {
66
+ status: 'succeeded',
67
+ ...(profile !== undefined ? { profile } : {}),
68
+ });
69
+ return {
70
+ ...actionResponse,
71
+ run: run.run,
72
+ execution: finished.execution,
73
+ };
74
+ }
75
+ catch (error) {
76
+ await this.finishBrowserRun(session, run.run.executionId, {
77
+ status: 'failed',
78
+ errorCode: error instanceof HostedClientError ? error.code : 'HOSTED_BROWSER_ACTION_FAILED',
79
+ ...(profile !== undefined ? { profile } : {}),
80
+ }).catch(() => undefined);
81
+ throw error;
82
+ }
83
+ }
84
+ async request(path, init = {}) {
85
+ const response = await this.fetchImpl(`${this.apiBaseUrl}${path}`, {
86
+ ...init,
87
+ headers: {
88
+ accept: 'application/json',
89
+ ...(init.body ? { 'content-type': 'application/json' } : {}),
90
+ authorization: `Bearer ${this.apiKey}`,
91
+ ...(init.headers ?? {}),
92
+ },
93
+ });
94
+ const text = await response.text();
95
+ const body = text ? parseJson(text) : {};
96
+ if (!response.ok || isHostedError(body)) {
97
+ const error = isHostedError(body)
98
+ ? body.error
99
+ : { code: `HTTP_${response.status}`, message: `Webcmd Cloud request failed with HTTP ${response.status}.` };
100
+ throw new HostedClientError(error.code || `HTTP_${response.status}`, error.message || `Webcmd Cloud request failed with HTTP ${response.status}.`, error.help ?? error.hint, normalizeExitCode(error.exitCode, response.status === 401 ? EXIT_CODES.NOPERM : EXIT_CODES.GENERIC_ERROR));
101
+ }
102
+ return body;
103
+ }
104
+ }
105
+ function normalizeExitCode(value, fallback) {
106
+ const allowed = new Set(Object.values(EXIT_CODES));
107
+ return value !== undefined && allowed.has(value) ? value : fallback;
108
+ }
109
+ function parseJson(text) {
110
+ try {
111
+ return JSON.parse(text);
112
+ }
113
+ catch {
114
+ throw new HostedClientError('HOSTED_PROTOCOL', 'Webcmd Cloud returned non-JSON response.');
115
+ }
116
+ }
117
+ function isHostedError(value) {
118
+ return !!value
119
+ && typeof value === 'object'
120
+ && value.ok === false
121
+ && typeof value.error === 'object';
122
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { HostedClient } from './client.js';
3
+ describe('HostedClient', () => {
4
+ it('sends bearer auth and parses hosted manifest', async () => {
5
+ const requests = [];
6
+ const client = new HostedClient({
7
+ apiBaseUrl: 'https://api.example.com/',
8
+ apiKey: 'wcmd_live_test',
9
+ fetchImpl: async (url, init) => {
10
+ requests.push({
11
+ url: String(url),
12
+ authorization: new Headers(init?.headers).get('authorization'),
13
+ });
14
+ return new Response(JSON.stringify({
15
+ ok: true,
16
+ manifest: { userId: 'user_demo', generatedAt: 'now', commands: [] },
17
+ }), { status: 200 });
18
+ },
19
+ });
20
+ await expect(client.getManifest()).resolves.toEqual({
21
+ userId: 'user_demo',
22
+ generatedAt: 'now',
23
+ commands: [],
24
+ });
25
+ expect(requests).toEqual([{ url: 'https://api.example.com/v1/manifest', authorization: 'Bearer wcmd_live_test' }]);
26
+ });
27
+ it('maps hosted error envelopes to CliError-compatible errors', async () => {
28
+ const client = new HostedClient({
29
+ apiBaseUrl: 'https://api.example.com',
30
+ apiKey: 'bad',
31
+ fetchImpl: async () => new Response(JSON.stringify({
32
+ ok: false,
33
+ error: {
34
+ code: 'UNAUTHORIZED',
35
+ message: 'Invalid key',
36
+ help: 'Run setup',
37
+ exitCode: 77,
38
+ },
39
+ }), { status: 401 }),
40
+ });
41
+ await expect(client.getManifest()).rejects.toMatchObject({
42
+ code: 'UNAUTHORIZED',
43
+ message: 'Invalid key',
44
+ hint: 'Run setup',
45
+ exitCode: 77,
46
+ });
47
+ });
48
+ it('runs hosted browser lifecycle calls and finishes the execution', async () => {
49
+ const requests = [];
50
+ const client = new HostedClient({
51
+ apiBaseUrl: 'https://api.example.com',
52
+ apiKey: 'wcmd_live_test',
53
+ fetchImpl: async (url, init) => {
54
+ requests.push({
55
+ url: String(url),
56
+ body: init?.body ? JSON.parse(String(init.body)) : undefined,
57
+ });
58
+ if (String(url).endsWith('/runs')) {
59
+ return new Response(JSON.stringify({
60
+ ok: true,
61
+ run: {
62
+ executionId: 'exec_1',
63
+ session: 'work',
64
+ profile: { id: 'profile_default', displayName: 'default' },
65
+ },
66
+ }), { status: 201 });
67
+ }
68
+ if (String(url).endsWith('/actions')) {
69
+ return new Response(JSON.stringify({
70
+ ok: true,
71
+ result: { url: 'https://example.com' },
72
+ columns: ['url'],
73
+ trace: null,
74
+ }), { status: 200 });
75
+ }
76
+ return new Response(JSON.stringify({
77
+ ok: true,
78
+ execution: { id: 'exec_1', status: 'succeeded' },
79
+ }), { status: 200 });
80
+ },
81
+ });
82
+ await expect(client.runBrowserAction('work', {
83
+ command: 'browser/open',
84
+ action: 'navigate',
85
+ args: { url: 'https://example.com' },
86
+ profile: 'default',
87
+ windowMode: 'background',
88
+ })).resolves.toMatchObject({
89
+ result: { url: 'https://example.com' },
90
+ execution: { id: 'exec_1', status: 'succeeded' },
91
+ });
92
+ expect(requests).toEqual([
93
+ {
94
+ url: 'https://api.example.com/v1/browser/work/runs',
95
+ body: {
96
+ command: 'browser/open',
97
+ args: { url: 'https://example.com' },
98
+ profile: 'default',
99
+ windowMode: 'background',
100
+ },
101
+ },
102
+ {
103
+ url: 'https://api.example.com/v1/browser/work/runs/exec_1/actions',
104
+ body: {
105
+ action: 'navigate',
106
+ args: { url: 'https://example.com' },
107
+ profile: 'default',
108
+ },
109
+ },
110
+ {
111
+ url: 'https://api.example.com/v1/browser/work/runs/exec_1/finish',
112
+ body: {
113
+ status: 'succeeded',
114
+ profile: 'default',
115
+ },
116
+ },
117
+ ]);
118
+ });
119
+ });
@@ -0,0 +1,50 @@
1
+ import * as fs from 'node:fs';
2
+ export interface HostedManifestCache {
3
+ fetchedAt: string;
4
+ manifest: unknown;
5
+ }
6
+ export type WebcmdConfig = {
7
+ mode: 'local';
8
+ updatedAt: string;
9
+ } | {
10
+ mode: 'hosted';
11
+ updatedAt: string;
12
+ hosted: {
13
+ apiBaseUrl: string;
14
+ apiKey: string;
15
+ manifestCache?: HostedManifestCache;
16
+ };
17
+ };
18
+ export interface ConfigIo {
19
+ readFileSync?: typeof fs.readFileSync;
20
+ writeFileSync?: typeof fs.writeFileSync;
21
+ mkdirSync?: typeof fs.mkdirSync;
22
+ chmodSync?: typeof fs.chmodSync;
23
+ existsSync?: typeof fs.existsSync;
24
+ env?: NodeJS.ProcessEnv;
25
+ homeDir?: string;
26
+ now?: () => Date;
27
+ }
28
+ export declare function getConfigDir(io?: Pick<ConfigIo, 'env' | 'homeDir'>): string;
29
+ export declare function getConfigPath(io?: Pick<ConfigIo, 'env' | 'homeDir'>): string;
30
+ export declare function loadWebcmdConfig(io?: ConfigIo): WebcmdConfig;
31
+ export declare function saveWebcmdConfig(config: WebcmdConfig, io?: ConfigIo): void;
32
+ export type LocalWebcmdConfig = Extract<WebcmdConfig, {
33
+ mode: 'local';
34
+ }>;
35
+ export type HostedWebcmdConfig = Extract<WebcmdConfig, {
36
+ mode: 'hosted';
37
+ }>;
38
+ export declare function makeLocalConfig(now?: Date): LocalWebcmdConfig;
39
+ export declare function makeHostedConfig(input: {
40
+ apiBaseUrl: string;
41
+ apiKey: string;
42
+ manifestCache?: HostedManifestCache;
43
+ now?: Date;
44
+ }): HostedWebcmdConfig;
45
+ export declare function normalizeApiBaseUrl(raw: string): string;
46
+ export declare function defaultHostedApiBaseUrl(env?: NodeJS.ProcessEnv): string;
47
+ export declare function isHostedConfig(config: WebcmdConfig): config is Extract<WebcmdConfig, {
48
+ mode: 'hosted';
49
+ }>;
50
+ export declare function shouldUseHostedMode(io?: ConfigIo): boolean;
@@ -0,0 +1,90 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { CONFIG_DIR_NAME, ENV_PREFIX } from '../brand.js';
5
+ export function getConfigDir(io = {}) {
6
+ const env = io.env ?? process.env;
7
+ return env[`${ENV_PREFIX}_CONFIG_DIR`] || path.join(io.homeDir ?? os.homedir(), CONFIG_DIR_NAME);
8
+ }
9
+ export function getConfigPath(io = {}) {
10
+ return path.join(getConfigDir(io), 'config.json');
11
+ }
12
+ function parseConfig(raw) {
13
+ const parsed = JSON.parse(raw);
14
+ if (parsed.mode === 'local' && typeof parsed.updatedAt === 'string') {
15
+ return { mode: 'local', updatedAt: parsed.updatedAt };
16
+ }
17
+ if (parsed.mode === 'hosted'
18
+ && typeof parsed.updatedAt === 'string'
19
+ && typeof parsed.hosted?.apiBaseUrl === 'string'
20
+ && typeof parsed.hosted?.apiKey === 'string') {
21
+ return {
22
+ mode: 'hosted',
23
+ updatedAt: parsed.updatedAt,
24
+ hosted: {
25
+ apiBaseUrl: parsed.hosted.apiBaseUrl,
26
+ apiKey: parsed.hosted.apiKey,
27
+ ...(parsed.hosted.manifestCache ? { manifestCache: parsed.hosted.manifestCache } : {}),
28
+ },
29
+ };
30
+ }
31
+ return { mode: 'local', updatedAt: new Date(0).toISOString() };
32
+ }
33
+ export function loadWebcmdConfig(io = {}) {
34
+ const readFileSync = io.readFileSync ?? fs.readFileSync;
35
+ try {
36
+ return parseConfig(readFileSync(getConfigPath(io), 'utf-8'));
37
+ }
38
+ catch {
39
+ return { mode: 'local', updatedAt: new Date(0).toISOString() };
40
+ }
41
+ }
42
+ export function saveWebcmdConfig(config, io = {}) {
43
+ const writeFileSync = io.writeFileSync ?? fs.writeFileSync;
44
+ const mkdirSync = io.mkdirSync ?? fs.mkdirSync;
45
+ const chmodSync = io.chmodSync ?? fs.chmodSync;
46
+ const target = getConfigPath(io);
47
+ mkdirSync(path.dirname(target), { recursive: true });
48
+ writeFileSync(target, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
49
+ try {
50
+ chmodSync(target, 0o600);
51
+ }
52
+ catch {
53
+ // Windows and unusual filesystems may not support POSIX modes.
54
+ }
55
+ }
56
+ export function makeLocalConfig(now = new Date()) {
57
+ return {
58
+ mode: 'local',
59
+ updatedAt: now.toISOString(),
60
+ };
61
+ }
62
+ export function makeHostedConfig(input) {
63
+ return {
64
+ mode: 'hosted',
65
+ updatedAt: (input.now ?? new Date()).toISOString(),
66
+ hosted: {
67
+ apiBaseUrl: normalizeApiBaseUrl(input.apiBaseUrl),
68
+ apiKey: input.apiKey.trim(),
69
+ ...(input.manifestCache ? { manifestCache: input.manifestCache } : {}),
70
+ },
71
+ };
72
+ }
73
+ export function normalizeApiBaseUrl(raw) {
74
+ const value = raw.trim().replace(/\/+$/, '');
75
+ return value || defaultHostedApiBaseUrl();
76
+ }
77
+ export function defaultHostedApiBaseUrl(env = process.env) {
78
+ return normalizeConfiguredUrl(env.WEBCMD_CLOUD_API_URL) ?? 'https://api.webcmd.dev';
79
+ }
80
+ function normalizeConfiguredUrl(raw) {
81
+ if (!raw?.trim())
82
+ return undefined;
83
+ return raw.trim().replace(/\/+$/, '');
84
+ }
85
+ export function isHostedConfig(config) {
86
+ return config.mode === 'hosted';
87
+ }
88
+ export function shouldUseHostedMode(io = {}) {
89
+ return isHostedConfig(loadWebcmdConfig(io));
90
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ import { mkdtemp, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { defaultHostedApiBaseUrl, getConfigPath, isHostedConfig, loadWebcmdConfig, makeHostedConfig, makeLocalConfig, saveWebcmdConfig, } from './config.js';
6
+ let tempDir;
7
+ afterEach(async () => {
8
+ if (tempDir)
9
+ await rm(tempDir, { recursive: true, force: true });
10
+ tempDir = undefined;
11
+ });
12
+ describe('hosted config', () => {
13
+ it('defaults to local mode when config is absent', () => {
14
+ tempDir = join(tmpdir(), `webcmd-config-missing-${Date.now()}`);
15
+ expect(loadWebcmdConfig({ env: { WEBCMD_CONFIG_DIR: tempDir } })).toEqual({
16
+ mode: 'local',
17
+ updatedAt: '1970-01-01T00:00:00.000Z',
18
+ });
19
+ });
20
+ it('writes and reads hosted config', async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), 'webcmd-config-'));
22
+ const env = { WEBCMD_CONFIG_DIR: tempDir };
23
+ const config = makeHostedConfig({
24
+ apiBaseUrl: 'https://api.example.com/',
25
+ apiKey: 'wcmd_live_test',
26
+ now: new Date('2026-07-08T00:00:00.000Z'),
27
+ });
28
+ saveWebcmdConfig(config, { env });
29
+ expect(getConfigPath({ env })).toBe(join(tempDir, 'config.json'));
30
+ expect(loadWebcmdConfig({ env })).toEqual({
31
+ mode: 'hosted',
32
+ updatedAt: '2026-07-08T00:00:00.000Z',
33
+ hosted: {
34
+ apiBaseUrl: 'https://api.example.com',
35
+ apiKey: 'wcmd_live_test',
36
+ },
37
+ });
38
+ expect(isHostedConfig(loadWebcmdConfig({ env }))).toBe(true);
39
+ });
40
+ it('writes local config and resolves default API URL from env', () => {
41
+ expect(makeLocalConfig(new Date('2026-07-08T00:00:00.000Z'))).toEqual({
42
+ mode: 'local',
43
+ updatedAt: '2026-07-08T00:00:00.000Z',
44
+ });
45
+ expect(defaultHostedApiBaseUrl({ WEBCMD_CLOUD_API_URL: 'https://cloud.example.com/' }))
46
+ .toBe('https://cloud.example.com');
47
+ });
48
+ });
@@ -0,0 +1,9 @@
1
+ import type { HostedCommand, HostedManifest } from './types.js';
2
+ export declare function isLocalOnlyHostedCommand(command: HostedCommand): boolean;
3
+ export declare function hostedCommands(manifest: HostedManifest): HostedCommand[];
4
+ export declare function findHostedCommand(manifest: HostedManifest, site: string, name: string): HostedCommand | null;
5
+ export declare function hostedListRows(manifest: HostedManifest, structured: boolean): Record<string, unknown>[];
6
+ export declare function siteNames(manifest: HostedManifest): string[];
7
+ export declare function commandNamesForSite(manifest: HostedManifest, site: string): string[];
8
+ export declare function renderHostedSiteHelp(manifest: HostedManifest, site: string): string;
9
+ export declare function renderHostedCommandHelp(command: HostedCommand): string;
@@ -0,0 +1,92 @@
1
+ import { formatArgSummary } from '../serialization.js';
2
+ export function isLocalOnlyHostedCommand(command) {
3
+ return command.strategy.toUpperCase() === 'LOCAL';
4
+ }
5
+ export function hostedCommands(manifest) {
6
+ return manifest.commands
7
+ .filter((command) => !isLocalOnlyHostedCommand(command))
8
+ .sort((a, b) => a.command.localeCompare(b.command));
9
+ }
10
+ export function findHostedCommand(manifest, site, name) {
11
+ return manifest.commands.find((command) => {
12
+ return command.site === site && (command.name === name || command.aliases?.includes(name));
13
+ }) ?? null;
14
+ }
15
+ export function hostedListRows(manifest, structured) {
16
+ return hostedCommands(manifest).map((command) => {
17
+ const row = {
18
+ command: command.command,
19
+ site: command.site,
20
+ name: command.name,
21
+ aliases: command.aliases ?? [],
22
+ description: command.description,
23
+ access: command.access,
24
+ strategy: command.strategy.toLowerCase(),
25
+ browser: command.browser,
26
+ args: command.args,
27
+ columns: command.columns ?? [],
28
+ domain: command.domain ?? null,
29
+ };
30
+ if (structured)
31
+ return row;
32
+ return {
33
+ ...row,
34
+ aliases: command.aliases?.join(', ') ?? '',
35
+ args: formatArgSummary(command.args.map((arg) => ({
36
+ name: arg.name,
37
+ type: arg.type,
38
+ required: arg.required,
39
+ valueRequired: arg.valueRequired,
40
+ positional: arg.positional,
41
+ help: arg.help,
42
+ choices: arg.choices?.map(String),
43
+ default: arg.default,
44
+ }))),
45
+ };
46
+ });
47
+ }
48
+ export function siteNames(manifest) {
49
+ return [...new Set(hostedCommands(manifest).map((command) => command.site))].sort((a, b) => a.localeCompare(b));
50
+ }
51
+ export function commandNamesForSite(manifest, site) {
52
+ return hostedCommands(manifest)
53
+ .filter((command) => command.site === site)
54
+ .flatMap((command) => [command.name, ...(command.aliases ?? [])])
55
+ .sort((a, b) => a.localeCompare(b));
56
+ }
57
+ export function renderHostedSiteHelp(manifest, site) {
58
+ const commands = hostedCommands(manifest).filter((command) => command.site === site);
59
+ if (commands.length === 0)
60
+ return `Unknown hosted Webcmd site: ${site}\n`;
61
+ const lines = [`Usage: webcmd ${site} <command> [options]`, '', 'Commands:'];
62
+ for (const command of commands) {
63
+ const tag = `[${command.strategy.toLowerCase()}]`;
64
+ lines.push(` ${command.name.padEnd(18)} ${tag} ${command.description}`);
65
+ }
66
+ return `${lines.join('\n')}\n`;
67
+ }
68
+ export function renderHostedCommandHelp(command) {
69
+ const lines = [
70
+ `Usage: webcmd ${command.site} ${command.name} ${formatArgSummary(command.args.map((arg) => ({
71
+ name: arg.name,
72
+ type: arg.type,
73
+ required: arg.required,
74
+ valueRequired: arg.valueRequired,
75
+ positional: arg.positional,
76
+ help: arg.help,
77
+ choices: arg.choices?.map(String),
78
+ default: arg.default,
79
+ })))} [options]`,
80
+ '',
81
+ command.description,
82
+ '',
83
+ `Access: ${command.access}`,
84
+ `Strategy: ${command.strategy.toLowerCase()}`,
85
+ `Browser: ${command.browser ? 'yes' : 'no'}`,
86
+ ];
87
+ if (command.domain)
88
+ lines.push(`Domain: ${command.domain}`);
89
+ if (command.columns?.length)
90
+ lines.push(`Output columns: ${command.columns.join(', ')}`);
91
+ return `${lines.join('\n')}\n`;
92
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { commandNamesForSite, findHostedCommand, hostedListRows, renderHostedCommandHelp, renderHostedSiteHelp, siteNames, } from './manifest.js';
3
+ const manifest = {
4
+ userId: 'user_demo',
5
+ generatedAt: '2026-07-08T00:00:00.000Z',
6
+ commands: [
7
+ {
8
+ site: 'github',
9
+ name: 'whoami',
10
+ aliases: ['me'],
11
+ command: 'github/whoami',
12
+ description: 'Show GitHub identity',
13
+ access: 'read',
14
+ strategy: 'COOKIE',
15
+ browser: true,
16
+ args: [],
17
+ columns: ['username'],
18
+ domain: 'github.com',
19
+ },
20
+ {
21
+ site: 'docker',
22
+ name: 'ps',
23
+ command: 'docker/ps',
24
+ description: 'Local Docker containers',
25
+ access: 'read',
26
+ strategy: 'LOCAL',
27
+ browser: false,
28
+ args: [],
29
+ },
30
+ ],
31
+ };
32
+ describe('hosted manifest helpers', () => {
33
+ it('filters LOCAL commands from hosted list rows', () => {
34
+ expect(hostedListRows(manifest, true).map((row) => row.command)).toEqual(['github/whoami']);
35
+ });
36
+ it('finds canonical commands and aliases', () => {
37
+ expect(findHostedCommand(manifest, 'github', 'whoami')?.command).toBe('github/whoami');
38
+ expect(findHostedCommand(manifest, 'github', 'me')?.command).toBe('github/whoami');
39
+ });
40
+ it('renders hosted help and completion names from supported commands', () => {
41
+ expect(siteNames(manifest)).toEqual(['github']);
42
+ expect(commandNamesForSite(manifest, 'github')).toEqual(['me', 'whoami']);
43
+ expect(renderHostedSiteHelp(manifest, 'github')).toContain('whoami');
44
+ expect(renderHostedCommandHelp(manifest.commands[0])).toContain('Output columns: username');
45
+ });
46
+ });
@@ -0,0 +1,12 @@
1
+ import { type WebcmdConfig } from './config.js';
2
+ export interface HostedRunnerOptions {
3
+ config?: WebcmdConfig;
4
+ fetchImpl?: typeof fetch;
5
+ stdout?: NodeJS.WritableStream;
6
+ stderr?: NodeJS.WritableStream;
7
+ }
8
+ export interface HostedRunResult {
9
+ handled: boolean;
10
+ exitCode: number;
11
+ }
12
+ export declare function runHostedCli(argv: string[], opts?: HostedRunnerOptions): Promise<HostedRunResult>;