@agentrhq/webcmd 0.2.5 → 0.3.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.
- package/README.md +13 -2
- package/cli-manifest.json +217 -13
- package/clis/_shared/site-auth.js +3 -3
- package/clis/_shared/site-auth.test.js +4 -4
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/ask.test.js +11 -0
- package/clis/chatgpt/commands.test.js +102 -1
- package/clis/chatgpt/deep-research-result.js +45 -3
- package/clis/chatgpt/image.js +22 -2
- package/clis/chatgpt/project-file-add.js +24 -1
- package/clis/chatgpt/utils.js +258 -52
- package/clis/chatgpt/utils.test.js +259 -1
- package/clis/claude/ask.js +21 -1
- package/clis/facebook/feed.js +110 -24
- package/clis/facebook/feed.test.js +62 -0
- package/clis/facebook/search.js +185 -38
- package/clis/facebook/search.test.js +95 -50
- package/clis/instagram/explore.js +30 -12
- package/clis/instagram/explore.test.js +77 -0
- package/clis/instagram/post.js +14 -1
- package/clis/instagram/reel.js +13 -1
- package/clis/linkedin/company.js +153 -0
- package/clis/linkedin/company.test.js +111 -0
- package/clis/linkedin/connections.js +135 -0
- package/clis/linkedin/connections.test.js +141 -0
- package/clis/mercury/reimbursement-draft.js +12 -1
- package/clis/slock/whoami.test.js +2 -2
- package/clis/twitter/article-evaluate.test.js +27 -0
- package/clis/twitter/article.js +73 -16
- package/clis/twitter/article.test.js +209 -0
- package/clis/twitter/download.js +6 -1
- package/clis/twitter/post.js +14 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/profile.test.js +8 -0
- package/clis/twitter/quote.js +11 -1
- package/clis/twitter/reply.js +11 -1
- package/dist/src/browser/base-page.d.ts +26 -0
- package/dist/src/browser/base-page.js +23 -8
- package/dist/src/browser/base-page.test.js +45 -12
- package/dist/src/browser/command-catalog.d.ts +6 -0
- package/dist/src/browser/command-catalog.js +259 -0
- package/dist/src/browser/command-catalog.test.d.ts +1 -0
- package/dist/src/browser/command-catalog.test.js +79 -0
- package/dist/src/browser/daemon-client.d.ts +2 -1
- package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.js +36 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +10 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.js +25 -9
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +99 -4
- package/dist/src/browser/target-resolver.test.js +141 -1
- package/dist/src/build-manifest.d.ts +6 -0
- package/dist/src/build-manifest.js +18 -1
- package/dist/src/build-manifest.test.js +57 -1
- package/dist/src/builtin-command-surface.d.ts +9 -0
- package/dist/src/builtin-command-surface.js +16 -0
- package/dist/src/check-hosted-contract.test.d.ts +1 -0
- package/dist/src/check-hosted-contract.test.js +56 -0
- package/dist/src/cli.js +63 -110
- package/dist/src/cli.test.js +82 -6
- package/dist/src/command-presentation.d.ts +95 -0
- package/dist/src/command-presentation.js +486 -0
- package/dist/src/command-presentation.test.d.ts +1 -0
- package/dist/src/command-presentation.test.js +97 -0
- package/dist/src/command-surface.d.ts +43 -0
- package/dist/src/command-surface.js +205 -0
- package/dist/src/command-surface.test.d.ts +1 -0
- package/dist/src/command-surface.test.js +406 -0
- package/dist/src/commanderAdapter.d.ts +5 -1
- package/dist/src/commanderAdapter.js +12 -55
- package/dist/src/commanderAdapter.test.js +15 -3
- package/dist/src/commands/auth.js +3 -2
- package/dist/src/commands/auth.test.js +6 -6
- package/dist/src/community-plugin-sync.d.ts +11 -0
- package/dist/src/community-plugin-sync.js +138 -0
- package/dist/src/community-plugin-sync.test.d.ts +1 -0
- package/dist/src/community-plugin-sync.test.js +123 -0
- package/dist/src/completion-fast.d.ts +4 -4
- package/dist/src/completion-fast.js +15 -34
- package/dist/src/completion-shared.d.ts +4 -0
- package/dist/src/completion-shared.js +38 -0
- package/dist/src/completion.js +3 -27
- package/dist/src/completion.test.js +31 -2
- package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
- package/dist/src/docs-sync-review-cli.test.js +322 -0
- package/dist/src/docs-sync-review.d.ts +117 -0
- package/dist/src/docs-sync-review.js +475 -0
- package/dist/src/docs-sync-review.test.d.ts +1 -0
- package/dist/src/docs-sync-review.test.js +324 -0
- package/dist/src/errors.d.ts +12 -3
- package/dist/src/errors.js +11 -7
- package/dist/src/errors.test.js +14 -1
- package/dist/src/execution.d.ts +1 -2
- package/dist/src/execution.js +4 -45
- package/dist/src/generate-release-notes-cli.test.js +11 -1
- package/dist/src/help.d.ts +4 -0
- package/dist/src/help.js +50 -255
- package/dist/src/help.test.js +27 -1
- package/dist/src/hosted/args.d.ts +4 -0
- package/dist/src/hosted/args.js +4 -0
- package/dist/src/hosted/args.test.d.ts +1 -0
- package/dist/src/hosted/args.test.js +49 -0
- package/dist/src/hosted/availability.d.ts +13 -0
- package/dist/src/hosted/availability.js +16 -0
- package/dist/src/hosted/availability.test.d.ts +1 -0
- package/dist/src/hosted/availability.test.js +180 -0
- package/dist/src/hosted/browser-args.d.ts +18 -0
- package/dist/src/hosted/browser-args.js +152 -0
- package/dist/src/hosted/browser-args.test.d.ts +1 -0
- package/dist/src/hosted/browser-args.test.js +182 -0
- package/dist/src/hosted/client.d.ts +59 -0
- package/dist/src/hosted/client.js +510 -0
- package/dist/src/hosted/client.test.d.ts +1 -0
- package/dist/src/hosted/client.test.js +738 -0
- package/dist/src/hosted/config.d.ts +55 -0
- package/dist/src/hosted/config.js +112 -0
- package/dist/src/hosted/config.test.d.ts +1 -0
- package/dist/src/hosted/config.test.js +81 -0
- package/dist/src/hosted/contract.d.ts +94 -0
- package/dist/src/hosted/contract.js +208 -0
- package/dist/src/hosted/contract.test.d.ts +1 -0
- package/dist/src/hosted/contract.test.js +361 -0
- package/dist/src/hosted/credentials.d.ts +38 -0
- package/dist/src/hosted/credentials.js +248 -0
- package/dist/src/hosted/credentials.test.d.ts +1 -0
- package/dist/src/hosted/credentials.test.js +93 -0
- package/dist/src/hosted/file-contract.test.d.ts +1 -0
- package/dist/src/hosted/file-contract.test.js +174 -0
- package/dist/src/hosted/files.d.ts +29 -0
- package/dist/src/hosted/files.js +296 -0
- package/dist/src/hosted/files.test.d.ts +1 -0
- package/dist/src/hosted/files.test.js +231 -0
- package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
- package/dist/src/hosted/main-lifecycle.test.js +213 -0
- package/dist/src/hosted/manifest.d.ts +14 -0
- package/dist/src/hosted/manifest.js +47 -0
- package/dist/src/hosted/manifest.test.d.ts +1 -0
- package/dist/src/hosted/manifest.test.js +164 -0
- package/dist/src/hosted/output-parity.test.d.ts +1 -0
- package/dist/src/hosted/output-parity.test.js +108 -0
- package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
- package/dist/src/hosted/root-command-surface.test.js +646 -0
- package/dist/src/hosted/runner.d.ts +19 -0
- package/dist/src/hosted/runner.js +705 -0
- package/dist/src/hosted/runner.test.d.ts +1 -0
- package/dist/src/hosted/runner.test.js +1607 -0
- package/dist/src/hosted/setup.d.ts +10 -0
- package/dist/src/hosted/setup.js +79 -0
- package/dist/src/hosted/setup.test.d.ts +1 -0
- package/dist/src/hosted/setup.test.js +195 -0
- package/dist/src/hosted/types.d.ts +175 -0
- package/dist/src/hosted/types.js +1 -0
- package/dist/src/main.js +123 -97
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/output.d.ts +17 -2
- package/dist/src/output.js +88 -81
- package/dist/src/output.test.js +141 -51
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/plugin-create-cli.test.d.ts +1 -0
- package/dist/src/plugin-create-cli.test.js +37 -0
- package/dist/src/plugin-manifest.d.ts +12 -0
- package/dist/src/plugin-manifest.js +16 -0
- package/dist/src/plugin-manifest.test.js +19 -1
- package/dist/src/plugin-scaffold.d.ts +4 -1
- package/dist/src/plugin-scaffold.js +4 -1
- package/dist/src/plugin-scaffold.test.js +23 -8
- package/dist/src/plugin.js +4 -1
- package/dist/src/plugin.test.js +13 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/release-notes.d.ts +6 -1
- package/dist/src/release-notes.js +192 -4
- package/dist/src/release-notes.test.js +143 -4
- package/dist/src/root-command-surface.d.ts +31 -0
- package/dist/src/root-command-surface.js +106 -0
- package/dist/src/serialization.d.ts +1 -16
- package/dist/src/serialization.js +5 -55
- package/dist/src/stream-write.d.ts +12 -0
- package/dist/src/stream-write.js +91 -0
- package/dist/src/stream-write.test.d.ts +1 -0
- package/dist/src/stream-write.test.js +186 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.js +2 -8
- package/dist/src/utils.test.js +50 -0
- package/hosted-contract.json +36748 -0
- package/package.json +6 -1
- package/scripts/check-hosted-contract.mjs +108 -0
- package/scripts/docs-sync-review.ts +332 -0
- package/scripts/generate-release-notes.ts +1 -1
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/smart-search/SKILL.md +14 -3
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +21 -7
- package/clis/antigravity/SKILL.md +0 -38
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import type { HostedCredentialBackend } from './credentials.js';
|
|
3
|
+
export interface HostedManifestCache {
|
|
4
|
+
fetchedAt: string;
|
|
5
|
+
manifest: unknown;
|
|
6
|
+
}
|
|
7
|
+
export type WebcmdConfig = {
|
|
8
|
+
mode: 'local';
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
} | {
|
|
11
|
+
mode: 'hosted';
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
hosted: {
|
|
14
|
+
apiBaseUrl: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
apiKeyRef?: string;
|
|
17
|
+
credentialBackend?: HostedCredentialBackend;
|
|
18
|
+
manifestCache?: HostedManifestCache;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export interface ConfigIo {
|
|
22
|
+
readFileSync?: typeof fs.readFileSync;
|
|
23
|
+
writeFileSync?: typeof fs.writeFileSync;
|
|
24
|
+
mkdirSync?: typeof fs.mkdirSync;
|
|
25
|
+
chmodSync?: typeof fs.chmodSync;
|
|
26
|
+
existsSync?: typeof fs.existsSync;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
homeDir?: string;
|
|
29
|
+
now?: () => Date;
|
|
30
|
+
}
|
|
31
|
+
export declare function getConfigDir(io?: Pick<ConfigIo, 'env' | 'homeDir'>): string;
|
|
32
|
+
export declare function getConfigPath(io?: Pick<ConfigIo, 'env' | 'homeDir'>): string;
|
|
33
|
+
export declare function loadWebcmdConfig(io?: ConfigIo): WebcmdConfig;
|
|
34
|
+
export declare function saveWebcmdConfig(config: WebcmdConfig, io?: ConfigIo): void;
|
|
35
|
+
export type LocalWebcmdConfig = Extract<WebcmdConfig, {
|
|
36
|
+
mode: 'local';
|
|
37
|
+
}>;
|
|
38
|
+
export type HostedWebcmdConfig = Extract<WebcmdConfig, {
|
|
39
|
+
mode: 'hosted';
|
|
40
|
+
}>;
|
|
41
|
+
export declare function makeLocalConfig(now?: Date): LocalWebcmdConfig;
|
|
42
|
+
export declare function makeHostedConfig(input: {
|
|
43
|
+
apiBaseUrl: string;
|
|
44
|
+
apiKey?: string;
|
|
45
|
+
apiKeyRef?: string;
|
|
46
|
+
credentialBackend?: HostedCredentialBackend;
|
|
47
|
+
manifestCache?: HostedManifestCache;
|
|
48
|
+
now?: Date;
|
|
49
|
+
}): HostedWebcmdConfig;
|
|
50
|
+
export declare function normalizeApiBaseUrl(raw: string): string;
|
|
51
|
+
export declare function defaultHostedApiBaseUrl(env?: NodeJS.ProcessEnv): string;
|
|
52
|
+
export declare function isHostedConfig(config: WebcmdConfig): config is Extract<WebcmdConfig, {
|
|
53
|
+
mode: 'hosted';
|
|
54
|
+
}>;
|
|
55
|
+
export declare function shouldUseHostedMode(io?: ConfigIo): boolean;
|
|
@@ -0,0 +1,112 @@
|
|
|
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' || typeof parsed.hosted?.apiKeyRef === 'string')) {
|
|
21
|
+
const credentialBackend = readCredentialBackend(parsed.hosted.credentialBackend);
|
|
22
|
+
return {
|
|
23
|
+
mode: 'hosted',
|
|
24
|
+
updatedAt: parsed.updatedAt,
|
|
25
|
+
hosted: {
|
|
26
|
+
apiBaseUrl: parsed.hosted.apiBaseUrl,
|
|
27
|
+
...(typeof parsed.hosted.apiKey === 'string' ? { apiKey: parsed.hosted.apiKey } : {}),
|
|
28
|
+
...(typeof parsed.hosted.apiKeyRef === 'string' ? { apiKeyRef: parsed.hosted.apiKeyRef } : {}),
|
|
29
|
+
...(credentialBackend ? { credentialBackend } : {}),
|
|
30
|
+
...(parsed.hosted.manifestCache ? { manifestCache: parsed.hosted.manifestCache } : {}),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { mode: 'local', updatedAt: new Date(0).toISOString() };
|
|
35
|
+
}
|
|
36
|
+
export function loadWebcmdConfig(io = {}) {
|
|
37
|
+
const readFileSync = io.readFileSync ?? fs.readFileSync;
|
|
38
|
+
try {
|
|
39
|
+
return parseConfig(readFileSync(getConfigPath(io), 'utf-8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return { mode: 'local', updatedAt: new Date(0).toISOString() };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function saveWebcmdConfig(config, io = {}) {
|
|
46
|
+
const writeFileSync = io.writeFileSync ?? fs.writeFileSync;
|
|
47
|
+
const mkdirSync = io.mkdirSync ?? fs.mkdirSync;
|
|
48
|
+
const chmodSync = io.chmodSync ?? fs.chmodSync;
|
|
49
|
+
const target = getConfigPath(io);
|
|
50
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
51
|
+
writeFileSync(target, `${JSON.stringify(persistableConfig(config), null, 2)}\n`, { encoding: 'utf-8', mode: 0o600 });
|
|
52
|
+
try {
|
|
53
|
+
chmodSync(target, 0o600);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Windows and unusual filesystems may not support POSIX modes.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function makeLocalConfig(now = new Date()) {
|
|
60
|
+
return {
|
|
61
|
+
mode: 'local',
|
|
62
|
+
updatedAt: now.toISOString(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function makeHostedConfig(input) {
|
|
66
|
+
return {
|
|
67
|
+
mode: 'hosted',
|
|
68
|
+
updatedAt: (input.now ?? new Date()).toISOString(),
|
|
69
|
+
hosted: {
|
|
70
|
+
apiBaseUrl: normalizeApiBaseUrl(input.apiBaseUrl),
|
|
71
|
+
...(input.apiKey !== undefined ? { apiKey: input.apiKey.trim() } : {}),
|
|
72
|
+
...(input.apiKeyRef !== undefined ? { apiKeyRef: input.apiKeyRef } : {}),
|
|
73
|
+
...(input.credentialBackend !== undefined ? { credentialBackend: input.credentialBackend } : {}),
|
|
74
|
+
...(input.manifestCache ? { manifestCache: input.manifestCache } : {}),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function normalizeApiBaseUrl(raw) {
|
|
79
|
+
const value = raw.trim().replace(/\/+$/, '');
|
|
80
|
+
return value || defaultHostedApiBaseUrl();
|
|
81
|
+
}
|
|
82
|
+
export function defaultHostedApiBaseUrl(env = process.env) {
|
|
83
|
+
return normalizeConfiguredUrl(env.WEBCMD_CLOUD_API_URL) ?? 'https://api.webcmd.dev';
|
|
84
|
+
}
|
|
85
|
+
function normalizeConfiguredUrl(raw) {
|
|
86
|
+
if (!raw?.trim())
|
|
87
|
+
return undefined;
|
|
88
|
+
return raw.trim().replace(/\/+$/, '');
|
|
89
|
+
}
|
|
90
|
+
export function isHostedConfig(config) {
|
|
91
|
+
return config.mode === 'hosted';
|
|
92
|
+
}
|
|
93
|
+
export function shouldUseHostedMode(io = {}) {
|
|
94
|
+
return isHostedConfig(loadWebcmdConfig(io));
|
|
95
|
+
}
|
|
96
|
+
function readCredentialBackend(value) {
|
|
97
|
+
return value === 'os' || value === 'file-fallback' ? value : undefined;
|
|
98
|
+
}
|
|
99
|
+
function persistableConfig(config) {
|
|
100
|
+
if (config.mode !== 'hosted' || !config.hosted.apiKeyRef)
|
|
101
|
+
return config;
|
|
102
|
+
return {
|
|
103
|
+
mode: 'hosted',
|
|
104
|
+
updatedAt: config.updatedAt,
|
|
105
|
+
hosted: {
|
|
106
|
+
apiBaseUrl: config.hosted.apiBaseUrl,
|
|
107
|
+
apiKeyRef: config.hosted.apiKeyRef,
|
|
108
|
+
...(config.hosted.credentialBackend ? { credentialBackend: config.hosted.credentialBackend } : {}),
|
|
109
|
+
...(config.hosted.manifestCache ? { manifestCache: config.hosted.manifestCache } : {}),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } 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
|
+
import { resolveHostedApiKey } from './credentials.js';
|
|
7
|
+
let tempDir;
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
if (tempDir)
|
|
10
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
11
|
+
tempDir = undefined;
|
|
12
|
+
});
|
|
13
|
+
describe('hosted config', () => {
|
|
14
|
+
it('defaults to local mode when config is absent', () => {
|
|
15
|
+
tempDir = join(tmpdir(), `webcmd-config-missing-${Date.now()}`);
|
|
16
|
+
expect(loadWebcmdConfig({ env: { WEBCMD_CONFIG_DIR: tempDir } })).toEqual({
|
|
17
|
+
mode: 'local',
|
|
18
|
+
updatedAt: '1970-01-01T00:00:00.000Z',
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('writes and reads hosted config', async () => {
|
|
22
|
+
tempDir = await mkdtemp(join(tmpdir(), 'webcmd-config-'));
|
|
23
|
+
const env = { WEBCMD_CONFIG_DIR: tempDir };
|
|
24
|
+
const config = makeHostedConfig({
|
|
25
|
+
apiBaseUrl: 'https://api.example.com/',
|
|
26
|
+
apiKey: 'wcmd_live_test',
|
|
27
|
+
now: new Date('2026-07-08T00:00:00.000Z'),
|
|
28
|
+
});
|
|
29
|
+
saveWebcmdConfig(config, { env });
|
|
30
|
+
expect(getConfigPath({ env })).toBe(join(tempDir, 'config.json'));
|
|
31
|
+
expect(loadWebcmdConfig({ env })).toEqual({
|
|
32
|
+
mode: 'hosted',
|
|
33
|
+
updatedAt: '2026-07-08T00:00:00.000Z',
|
|
34
|
+
hosted: {
|
|
35
|
+
apiBaseUrl: 'https://api.example.com',
|
|
36
|
+
apiKey: 'wcmd_live_test',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
expect(isHostedConfig(loadWebcmdConfig({ env }))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('migrates a legacy inline API key without losing manifest cache', async () => {
|
|
42
|
+
tempDir = await mkdtemp(join(tmpdir(), 'webcmd-config-migrate-'));
|
|
43
|
+
const env = {
|
|
44
|
+
WEBCMD_CONFIG_DIR: tempDir,
|
|
45
|
+
WEBCMD_CREDENTIAL_BACKEND: 'file',
|
|
46
|
+
};
|
|
47
|
+
await writeFile(getConfigPath({ env }), JSON.stringify({
|
|
48
|
+
mode: 'hosted',
|
|
49
|
+
updatedAt: '2026-07-08T00:00:00.000Z',
|
|
50
|
+
hosted: {
|
|
51
|
+
apiBaseUrl: 'https://api.example.com',
|
|
52
|
+
apiKey: 'wcmd_legacy_secret',
|
|
53
|
+
manifestCache: {
|
|
54
|
+
fetchedAt: '2026-07-08T00:01:00.000Z',
|
|
55
|
+
manifest: { ok: true },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}, null, 2));
|
|
59
|
+
const loaded = loadWebcmdConfig({ env });
|
|
60
|
+
if (!isHostedConfig(loaded))
|
|
61
|
+
throw new Error('Expected hosted config');
|
|
62
|
+
const credential = await resolveHostedApiKey(loaded, { env, platform: 'linux' });
|
|
63
|
+
const persisted = await readFile(getConfigPath({ env }), 'utf8');
|
|
64
|
+
expect(credential).toMatchObject({
|
|
65
|
+
apiKey: 'wcmd_legacy_secret',
|
|
66
|
+
backend: 'file-fallback',
|
|
67
|
+
migrated: true,
|
|
68
|
+
});
|
|
69
|
+
expect(persisted).not.toContain('wcmd_legacy_secret');
|
|
70
|
+
expect(persisted).toContain('apiKeyRef');
|
|
71
|
+
expect(persisted).toContain('manifestCache');
|
|
72
|
+
});
|
|
73
|
+
it('writes local config and resolves default API URL from env', () => {
|
|
74
|
+
expect(makeLocalConfig(new Date('2026-07-08T00:00:00.000Z'))).toEqual({
|
|
75
|
+
mode: 'local',
|
|
76
|
+
updatedAt: '2026-07-08T00:00:00.000Z',
|
|
77
|
+
});
|
|
78
|
+
expect(defaultHostedApiBaseUrl({ WEBCMD_CLOUD_API_URL: 'https://cloud.example.com/' }))
|
|
79
|
+
.toBe('https://cloud.example.com');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Arg, CliCommand } from '../registry.js';
|
|
2
|
+
import { type HostedAvailability } from './availability.js';
|
|
3
|
+
export declare const HOSTED_CONTRACT_SCHEMA_VERSION: 1;
|
|
4
|
+
export type { HostedAvailability } from './availability.js';
|
|
5
|
+
export interface FileArgumentContract {
|
|
6
|
+
direction: 'input' | 'output';
|
|
7
|
+
pathKind: 'file' | 'directory';
|
|
8
|
+
multiple?: boolean;
|
|
9
|
+
separator?: ',';
|
|
10
|
+
contentTypes?: string[];
|
|
11
|
+
maxBytes?: number;
|
|
12
|
+
/** Local destination used when the adapter itself supplies an implicit path. */
|
|
13
|
+
defaultPath?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HostedFileArgumentContract {
|
|
16
|
+
name: string;
|
|
17
|
+
direction: 'input' | 'output';
|
|
18
|
+
pathKind: 'file' | 'directory';
|
|
19
|
+
multiple: boolean;
|
|
20
|
+
required: boolean;
|
|
21
|
+
separator?: ',';
|
|
22
|
+
contentTypes?: string[];
|
|
23
|
+
maxBytes?: number;
|
|
24
|
+
defaultPath?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface HostedArgumentContract {
|
|
27
|
+
name: string;
|
|
28
|
+
type: 'string' | 'int' | 'number' | 'boolean';
|
|
29
|
+
description: string;
|
|
30
|
+
positional: boolean;
|
|
31
|
+
required: boolean;
|
|
32
|
+
variadic: boolean;
|
|
33
|
+
default?: unknown;
|
|
34
|
+
choices?: string[];
|
|
35
|
+
}
|
|
36
|
+
export interface HostedOptionContract extends HostedArgumentContract {
|
|
37
|
+
name: string;
|
|
38
|
+
flags: string;
|
|
39
|
+
positional: false;
|
|
40
|
+
}
|
|
41
|
+
export type HostedSessionPolicy = 'create-or-reuse' | 'require-existing' | 'close-existing' | 'local-only';
|
|
42
|
+
export interface HostedBrowserCommandContract {
|
|
43
|
+
command: string;
|
|
44
|
+
aliases: string[];
|
|
45
|
+
description: string;
|
|
46
|
+
positionals: HostedArgumentContract[];
|
|
47
|
+
options: HostedArgumentContract[];
|
|
48
|
+
sessionPolicy: HostedSessionPolicy;
|
|
49
|
+
action?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface HostedContractCommand {
|
|
52
|
+
command: string;
|
|
53
|
+
site: string;
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
access: 'read' | 'write';
|
|
57
|
+
strategy: 'PUBLIC' | 'COOKIE' | 'INTERCEPT' | 'UI' | 'LOCAL';
|
|
58
|
+
browser: boolean;
|
|
59
|
+
domain?: string;
|
|
60
|
+
positionals: HostedArgumentContract[];
|
|
61
|
+
options: HostedArgumentContract[];
|
|
62
|
+
columns: string[];
|
|
63
|
+
aliases: string[];
|
|
64
|
+
defaultFormat: string;
|
|
65
|
+
example?: string;
|
|
66
|
+
fileArguments: HostedFileArgumentContract[];
|
|
67
|
+
sessionPolicy: HostedSessionPolicy;
|
|
68
|
+
availability: HostedAvailability;
|
|
69
|
+
}
|
|
70
|
+
export interface HostedContract {
|
|
71
|
+
schemaVersion: typeof HOSTED_CONTRACT_SCHEMA_VERSION;
|
|
72
|
+
webcmdVersion: string;
|
|
73
|
+
outputFormats: Array<'table' | 'plain' | 'json' | 'yaml' | 'md' | 'csv'>;
|
|
74
|
+
traceModes: Array<'off' | 'on' | 'retain-on-failure'>;
|
|
75
|
+
commonOptions: HostedOptionContract[];
|
|
76
|
+
commands: HostedContractCommand[];
|
|
77
|
+
browserCommands: HostedBrowserCommandContract[];
|
|
78
|
+
}
|
|
79
|
+
export interface HostedContractCommandInput {
|
|
80
|
+
site: string;
|
|
81
|
+
name: string;
|
|
82
|
+
aliases?: string[];
|
|
83
|
+
description: string;
|
|
84
|
+
access: 'read' | 'write';
|
|
85
|
+
example?: string;
|
|
86
|
+
domain?: string;
|
|
87
|
+
strategy?: string;
|
|
88
|
+
browser?: boolean;
|
|
89
|
+
args: Arg[];
|
|
90
|
+
columns?: string[];
|
|
91
|
+
defaultFormat?: CliCommand['defaultFormat'];
|
|
92
|
+
}
|
|
93
|
+
export declare function buildHostedContract(commands: readonly HostedContractCommandInput[], browserCatalogInput: readonly HostedBrowserCommandContract[], packageVersion: string): HostedContract;
|
|
94
|
+
export declare function serializeHostedContract(contract: HostedContract): string;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { commandHelpData } from '../help.js';
|
|
2
|
+
import { browserCommandCatalog } from '../browser/command-catalog.js';
|
|
3
|
+
import { deriveBrowserAvailability, deriveHostedAvailability, } from './availability.js';
|
|
4
|
+
export const HOSTED_CONTRACT_SCHEMA_VERSION = 1;
|
|
5
|
+
const KNOWN_SESSION_POLICIES = new Set([
|
|
6
|
+
'create-or-reuse',
|
|
7
|
+
'require-existing',
|
|
8
|
+
'close-existing',
|
|
9
|
+
'local-only',
|
|
10
|
+
]);
|
|
11
|
+
function sharedContractOptions() {
|
|
12
|
+
const metadata = commandHelpData({
|
|
13
|
+
site: '__contract__',
|
|
14
|
+
name: '__contract__',
|
|
15
|
+
description: '',
|
|
16
|
+
access: 'read',
|
|
17
|
+
strategy: 'public',
|
|
18
|
+
browser: false,
|
|
19
|
+
args: [],
|
|
20
|
+
});
|
|
21
|
+
const common = metadata.common_options;
|
|
22
|
+
const format = common.find(option => option.name === 'format');
|
|
23
|
+
const trace = common.find(option => option.name === 'trace');
|
|
24
|
+
if (!format?.choices || !trace?.choices) {
|
|
25
|
+
throw new Error('Shared format and trace option choices are required');
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
outputFormats: [...format.choices],
|
|
29
|
+
traceModes: [...trace.choices],
|
|
30
|
+
commonOptions: common.map(option => ({
|
|
31
|
+
name: option.name,
|
|
32
|
+
flags: option.flags,
|
|
33
|
+
type: option.flags.includes('<') || option.flags.includes('[') ? 'string' : 'boolean',
|
|
34
|
+
description: option.help ?? '',
|
|
35
|
+
positional: false,
|
|
36
|
+
required: false,
|
|
37
|
+
variadic: false,
|
|
38
|
+
...(option.default !== undefined ? { default: option.default } : {}),
|
|
39
|
+
...(option.choices?.length ? { choices: [...option.choices] } : {}),
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function normalizeArgumentType(type) {
|
|
44
|
+
switch ((type ?? 'string').toLowerCase()) {
|
|
45
|
+
case 'str':
|
|
46
|
+
case 'string':
|
|
47
|
+
return 'string';
|
|
48
|
+
case 'bool':
|
|
49
|
+
case 'boolean':
|
|
50
|
+
return 'boolean';
|
|
51
|
+
case 'int':
|
|
52
|
+
case 'integer':
|
|
53
|
+
return 'int';
|
|
54
|
+
case 'number':
|
|
55
|
+
case 'float':
|
|
56
|
+
return 'number';
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unsupported hosted argument type: ${type}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function normalizeArgument(arg) {
|
|
62
|
+
return {
|
|
63
|
+
name: arg.name,
|
|
64
|
+
type: normalizeArgumentType(arg.type),
|
|
65
|
+
description: arg.help ?? '',
|
|
66
|
+
positional: arg.positional === true,
|
|
67
|
+
required: arg.required === true,
|
|
68
|
+
variadic: false,
|
|
69
|
+
...(arg.default !== undefined ? { default: arg.default } : {}),
|
|
70
|
+
...(arg.choices?.length ? { choices: [...arg.choices] } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function normalizeStrategy(command) {
|
|
74
|
+
const raw = command.strategy ?? (command.browser === false ? 'public' : 'cookie');
|
|
75
|
+
const strategy = raw.toUpperCase();
|
|
76
|
+
if (strategy === 'PUBLIC' || strategy === 'COOKIE' || strategy === 'INTERCEPT'
|
|
77
|
+
|| strategy === 'UI' || strategy === 'LOCAL') {
|
|
78
|
+
return strategy;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Unsupported hosted strategy for ${command.site}/${command.name}: ${raw}`);
|
|
81
|
+
}
|
|
82
|
+
function normalizeDefaultFormat(format) {
|
|
83
|
+
if (format === 'yml')
|
|
84
|
+
return 'yaml';
|
|
85
|
+
if (format === 'markdown')
|
|
86
|
+
return 'md';
|
|
87
|
+
return format ?? 'table';
|
|
88
|
+
}
|
|
89
|
+
function normalizeFileArguments(command) {
|
|
90
|
+
return command.args.flatMap((arg) => {
|
|
91
|
+
if (!arg.file)
|
|
92
|
+
return [];
|
|
93
|
+
if (arg.file.direction !== 'input' && arg.file.direction !== 'output') {
|
|
94
|
+
throw new Error(`File argument ${command.site}/${command.name} ${arg.name} must declare direction`);
|
|
95
|
+
}
|
|
96
|
+
if (arg.file.pathKind !== 'file' && arg.file.pathKind !== 'directory') {
|
|
97
|
+
throw new Error(`File argument ${command.site}/${command.name} ${arg.name} must declare pathKind`);
|
|
98
|
+
}
|
|
99
|
+
if (arg.file.separator !== undefined && arg.file.separator !== ',') {
|
|
100
|
+
throw new Error(`File argument ${command.site}/${command.name} ${arg.name} declares unsupported separator`);
|
|
101
|
+
}
|
|
102
|
+
return [{
|
|
103
|
+
name: arg.name,
|
|
104
|
+
direction: arg.file.direction,
|
|
105
|
+
pathKind: arg.file.pathKind,
|
|
106
|
+
multiple: arg.file.multiple === true,
|
|
107
|
+
required: arg.required === true,
|
|
108
|
+
...(arg.file.separator !== undefined ? { separator: arg.file.separator } : {}),
|
|
109
|
+
...(arg.file.contentTypes?.length ? { contentTypes: [...arg.file.contentTypes] } : {}),
|
|
110
|
+
...(arg.file.maxBytes !== undefined ? { maxBytes: arg.file.maxBytes } : {}),
|
|
111
|
+
...(arg.file.defaultPath !== undefined ? { defaultPath: arg.file.defaultPath } : {}),
|
|
112
|
+
}];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function assertUniqueAdapterCommands(commands) {
|
|
116
|
+
const canonical = new Set();
|
|
117
|
+
for (const command of commands) {
|
|
118
|
+
const key = `${command.site}/${command.name}`;
|
|
119
|
+
if (canonical.has(key))
|
|
120
|
+
throw new Error(`Duplicate canonical command: ${key}`);
|
|
121
|
+
canonical.add(key);
|
|
122
|
+
}
|
|
123
|
+
const invocations = new Set(canonical);
|
|
124
|
+
for (const command of commands) {
|
|
125
|
+
for (const alias of command.aliases ?? []) {
|
|
126
|
+
const key = `${command.site}/${alias}`;
|
|
127
|
+
if (invocations.has(key))
|
|
128
|
+
throw new Error(`Duplicate command alias: ${key}`);
|
|
129
|
+
invocations.add(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function normalizeBrowserCatalog(browserCatalog) {
|
|
134
|
+
const canonical = new Set();
|
|
135
|
+
for (const browserCommand of browserCatalog) {
|
|
136
|
+
if (canonical.has(browserCommand.command)) {
|
|
137
|
+
throw new Error(`Duplicate canonical browser command: ${browserCommand.command}`);
|
|
138
|
+
}
|
|
139
|
+
canonical.add(browserCommand.command);
|
|
140
|
+
if (!KNOWN_SESSION_POLICIES.has(browserCommand.sessionPolicy)) {
|
|
141
|
+
throw new Error(`Browser command ${browserCommand.command} must declare a known session policy`);
|
|
142
|
+
}
|
|
143
|
+
const availability = deriveBrowserAvailability(browserCommand.command);
|
|
144
|
+
if (availability.mode === 'local-only' && browserCommand.sessionPolicy !== 'local-only') {
|
|
145
|
+
throw new Error(`Browser command ${browserCommand.command} must use local-only session policy`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const invocations = new Set(canonical);
|
|
149
|
+
for (const browserCommand of browserCatalog) {
|
|
150
|
+
for (const alias of browserCommand.aliases ?? []) {
|
|
151
|
+
if (invocations.has(alias))
|
|
152
|
+
throw new Error(`Duplicate browser command alias: ${alias}`);
|
|
153
|
+
invocations.add(alias);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return browserCatalog
|
|
157
|
+
.map(browserCommand => ({
|
|
158
|
+
command: browserCommand.command,
|
|
159
|
+
aliases: [...(browserCommand.aliases ?? [])],
|
|
160
|
+
description: browserCommand.description,
|
|
161
|
+
positionals: browserCommand.positionals.map(arg => ({ ...arg })),
|
|
162
|
+
options: browserCommand.options.map(arg => ({ ...arg })),
|
|
163
|
+
sessionPolicy: browserCommand.sessionPolicy,
|
|
164
|
+
...(browserCommand.action !== undefined ? { action: browserCommand.action } : {}),
|
|
165
|
+
}))
|
|
166
|
+
.sort((a, b) => a.command.localeCompare(b.command));
|
|
167
|
+
}
|
|
168
|
+
export function buildHostedContract(commands, browserCatalogInput, packageVersion) {
|
|
169
|
+
assertUniqueAdapterCommands(commands);
|
|
170
|
+
const browserCatalog = browserCatalogInput.length > 0
|
|
171
|
+
? browserCatalogInput
|
|
172
|
+
: browserCommandCatalog;
|
|
173
|
+
const shared = sharedContractOptions();
|
|
174
|
+
const contractCommands = commands.map((command) => {
|
|
175
|
+
const availability = deriveHostedAvailability(command);
|
|
176
|
+
return {
|
|
177
|
+
command: `${command.site}/${command.name}`,
|
|
178
|
+
site: command.site,
|
|
179
|
+
name: command.name,
|
|
180
|
+
description: command.description,
|
|
181
|
+
access: command.access,
|
|
182
|
+
strategy: normalizeStrategy(command),
|
|
183
|
+
browser: command.browser !== false,
|
|
184
|
+
...(command.domain !== undefined ? { domain: command.domain } : {}),
|
|
185
|
+
positionals: command.args.filter(arg => arg.positional).map(normalizeArgument),
|
|
186
|
+
options: command.args.filter(arg => !arg.positional).map(normalizeArgument),
|
|
187
|
+
columns: [...(command.columns ?? [])],
|
|
188
|
+
aliases: [...(command.aliases ?? [])],
|
|
189
|
+
defaultFormat: normalizeDefaultFormat(command.defaultFormat),
|
|
190
|
+
...(command.example !== undefined ? { example: command.example } : {}),
|
|
191
|
+
fileArguments: normalizeFileArguments(command),
|
|
192
|
+
sessionPolicy: availability.mode === 'hosted' ? 'create-or-reuse' : 'local-only',
|
|
193
|
+
availability,
|
|
194
|
+
};
|
|
195
|
+
}).sort((a, b) => a.command.localeCompare(b.command));
|
|
196
|
+
return {
|
|
197
|
+
schemaVersion: HOSTED_CONTRACT_SCHEMA_VERSION,
|
|
198
|
+
webcmdVersion: packageVersion,
|
|
199
|
+
outputFormats: shared.outputFormats,
|
|
200
|
+
traceModes: shared.traceModes,
|
|
201
|
+
commonOptions: shared.commonOptions,
|
|
202
|
+
commands: contractCommands,
|
|
203
|
+
browserCommands: normalizeBrowserCatalog(browserCatalog),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export function serializeHostedContract(contract) {
|
|
207
|
+
return `${JSON.stringify(contract, null, 2)}\n`;
|
|
208
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|