@agentrhq/webcmd 0.3.0 → 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/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/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/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/session-manager.d.ts +7 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -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 +58 -106
- package/dist/src/cli.test.js +71 -1
- 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/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 +7 -2
- 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 +2 -7
- package/dist/src/hosted/args.js +2 -99
- package/dist/src/hosted/args.test.js +15 -1
- 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 +31 -2
- package/dist/src/hosted/client.js +441 -53
- package/dist/src/hosted/client.test.js +651 -32
- package/dist/src/hosted/config.d.ts +7 -2
- package/dist/src/hosted/config.js +26 -4
- package/dist/src/hosted/config.test.js +34 -1
- 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 +5 -0
- package/dist/src/hosted/manifest.js +21 -66
- package/dist/src/hosted/manifest.test.js +120 -2
- 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 +7 -0
- package/dist/src/hosted/runner.js +554 -253
- package/dist/src/hosted/runner.test.js +1471 -53
- package/dist/src/hosted/setup.d.ts +3 -2
- package/dist/src/hosted/setup.js +45 -15
- package/dist/src/hosted/setup.test.js +133 -6
- package/dist/src/hosted/types.d.ts +101 -23
- package/dist/src/main.js +120 -108
- 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.js +12 -4
- package/dist/src/release-notes.test.js +27 -15
- 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/sync-community-plugins.ts +14 -0
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +7 -0
|
@@ -10,50 +10,20 @@
|
|
|
10
10
|
* All execution logic lives in execution.ts.
|
|
11
11
|
*/
|
|
12
12
|
import { log } from './logger.js';
|
|
13
|
-
import yaml from 'js-yaml';
|
|
14
13
|
import { fullName, getRegistry } from './registry.js';
|
|
15
|
-
import { render as renderOutput } from './output.js';
|
|
14
|
+
import { formatErrorEnvelope, render as renderOutput } from './output.js';
|
|
16
15
|
import { executeCommand, prepareCommandArgs } from './execution.js';
|
|
16
|
+
import { configureCommandSurface, parseOutputFormat } from './command-surface.js';
|
|
17
17
|
import { commandHelpData, formatCommandHelpText, formatCommandListTerm, formatSiteCommandDescription, formatSiteHelpText, getRequestedHelpFormat, renderStructuredHelp, siteHelpData, } from './help.js';
|
|
18
18
|
import { CliError, EXIT_CODES, toEnvelope, } from './errors.js';
|
|
19
|
-
|
|
20
|
-
* Register a single CliCommand as a Commander subcommand.
|
|
21
|
-
*/
|
|
22
|
-
export function registerCommandToProgram(siteCmd, cmd) {
|
|
19
|
+
export function registerCommandToProgram(siteCmd, cmd, runtime = {}) {
|
|
23
20
|
if (siteCmd.commands.some((c) => c.name() === cmd.name))
|
|
24
21
|
return;
|
|
25
22
|
const subCmd = siteCmd.command(cmd.name).description(formatSiteCommandDescription(cmd));
|
|
26
23
|
if (cmd.aliases?.length)
|
|
27
24
|
subCmd.aliases(cmd.aliases);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
for (const arg of cmd.args) {
|
|
31
|
-
if (arg.positional) {
|
|
32
|
-
const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
|
|
33
|
-
subCmd.argument(bracket, arg.help ?? '');
|
|
34
|
-
positionalArgs.push(arg);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
const expectsValue = arg.required || arg.valueRequired;
|
|
38
|
-
const flag = expectsValue ? `--${arg.name} <value>` : `--${arg.name} [value]`;
|
|
39
|
-
if (arg.required)
|
|
40
|
-
subCmd.requiredOption(flag, arg.help ?? '');
|
|
41
|
-
else if (arg.default != null)
|
|
42
|
-
subCmd.option(flag, arg.help ?? '', String(arg.default));
|
|
43
|
-
else
|
|
44
|
-
subCmd.option(flag, arg.help ?? '');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
subCmd
|
|
48
|
-
.option('-f, --format <fmt>', 'Output format: table, plain, json, yaml, md, csv', 'table')
|
|
49
|
-
.option('--trace <mode>', 'Trace capture: off, on, retain-on-failure', 'off')
|
|
50
|
-
.option('-v, --verbose', 'Debug output', false);
|
|
51
|
-
if (cmd.browser) {
|
|
52
|
-
subCmd
|
|
53
|
-
.option('--window <mode>', 'Browser window mode: foreground or background')
|
|
54
|
-
.option('--site-session <mode>', 'Adapter site session lifecycle: ephemeral or persistent')
|
|
55
|
-
.option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
|
|
56
|
-
}
|
|
25
|
+
const positionalArgs = cmd.args.filter((arg) => arg.positional);
|
|
26
|
+
configureCommandSurface(subCmd, cmd);
|
|
57
27
|
const originalHelpInformation = subCmd.helpInformation.bind(subCmd);
|
|
58
28
|
subCmd.helpInformation = ((contextOptions) => {
|
|
59
29
|
const format = getRequestedHelpFormat();
|
|
@@ -67,7 +37,8 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
67
37
|
subCmd.action(async (...actionArgs) => {
|
|
68
38
|
const actionOpts = actionArgs[positionalArgs.length] ?? {};
|
|
69
39
|
const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts : {};
|
|
70
|
-
const
|
|
40
|
+
const now = runtime.now ?? Date.now;
|
|
41
|
+
const startTime = now();
|
|
71
42
|
// ── Execute + render ────────────────────────────────────────────────
|
|
72
43
|
try {
|
|
73
44
|
// ── Collect kwargs ────────────────────────────────────────────────
|
|
@@ -99,7 +70,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
99
70
|
}
|
|
100
71
|
const kwargs = prepareCommandArgs(cmd, rawKwargs);
|
|
101
72
|
const verbose = optionsRecord.verbose === true;
|
|
102
|
-
let format =
|
|
73
|
+
let format = parseOutputFormat(optionsRecord.format ?? 'table');
|
|
103
74
|
const formatExplicit = subCmd.getOptionValueSource('format') === 'cli';
|
|
104
75
|
if (verbose)
|
|
105
76
|
process.env.WEBCMD_VERBOSE = '1';
|
|
@@ -122,14 +93,15 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
122
93
|
if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
123
94
|
log.warn('Command returned an empty result.');
|
|
124
95
|
}
|
|
125
|
-
renderOutput(result, {
|
|
96
|
+
await renderOutput(result, {
|
|
126
97
|
fmt: format,
|
|
127
98
|
fmtExplicit: formatExplicit,
|
|
128
99
|
columns: resolved.columns,
|
|
129
100
|
title: `${resolved.site}/${resolved.name}`,
|
|
130
|
-
elapsed: (
|
|
101
|
+
elapsed: (now() - startTime) / 1000,
|
|
131
102
|
source: fullName(resolved),
|
|
132
103
|
footerExtra: resolved.footerExtra?.(kwargs),
|
|
104
|
+
...(runtime.stdout ? { stdout: runtime.stdout } : {}),
|
|
133
105
|
});
|
|
134
106
|
}
|
|
135
107
|
catch (err) {
|
|
@@ -145,28 +117,13 @@ function resolveExitCode(err) {
|
|
|
145
117
|
return EXIT_CODES.GENERIC_ERROR;
|
|
146
118
|
}
|
|
147
119
|
// ── Error rendering ─────────────────────────────────────────────────────────
|
|
148
|
-
/** Emit AutoFix hint for repairable adapter errors (skipped if trace already exported). */
|
|
149
|
-
function emitAutoFixHint(envelope, cmdName, traceMode) {
|
|
150
|
-
if (traceMode === 'on' || traceMode === 'retain-on-failure')
|
|
151
|
-
return envelope;
|
|
152
|
-
const runnable = cmdName.replace('/', ' ');
|
|
153
|
-
return envelope
|
|
154
|
-
+ `# AutoFix: re-run with --trace=retain-on-failure for trace artifact\n`
|
|
155
|
-
+ `# webcmd ${runnable} --trace retain-on-failure\n`;
|
|
156
|
-
}
|
|
157
120
|
function renderError(err, cmdName, verbose, traceMode) {
|
|
158
121
|
const envelope = toEnvelope(err);
|
|
159
122
|
// In verbose mode, include stack trace for debugging
|
|
160
123
|
if (verbose && err instanceof Error && err.stack) {
|
|
161
124
|
envelope.error.stack = err.stack;
|
|
162
125
|
}
|
|
163
|
-
|
|
164
|
-
// Append AutoFix hint for repairable errors
|
|
165
|
-
const code = envelope.error.code;
|
|
166
|
-
if (code === 'SELECTOR' || code === 'EMPTY_RESULT' || code === 'ADAPTER_LOAD' || code === 'UNKNOWN') {
|
|
167
|
-
output = emitAutoFixHint(output, cmdName, traceMode);
|
|
168
|
-
}
|
|
169
|
-
process.stderr.write(output);
|
|
126
|
+
process.stderr.write(formatErrorEnvelope(envelope, { cmdName, traceMode }));
|
|
170
127
|
}
|
|
171
128
|
/**
|
|
172
129
|
* Register all commands from the registry onto a Commander program.
|
|
@@ -12,9 +12,13 @@ vi.mock('./execution.js', async () => {
|
|
|
12
12
|
executeCommand: mockExecuteCommand,
|
|
13
13
|
};
|
|
14
14
|
});
|
|
15
|
-
vi.mock('./output.js', () =>
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
vi.mock('./output.js', async () => {
|
|
16
|
+
const actual = await vi.importActual('./output.js');
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
render: mockRenderOutput,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
18
22
|
import { registerCommandToProgram } from './commanderAdapter.js';
|
|
19
23
|
describe('commanderAdapter arg passing', () => {
|
|
20
24
|
const cmd = {
|
|
@@ -248,6 +252,14 @@ describe('commanderAdapter default formats', () => {
|
|
|
248
252
|
await program.parseAsync(['node', 'webcmd', 'gemini', 'ask', '--format', 'json']);
|
|
249
253
|
expect(mockRenderOutput).toHaveBeenCalledWith([{ response: 'hello' }], expect.objectContaining({ fmt: 'json' }));
|
|
250
254
|
});
|
|
255
|
+
it('preserves the legacy fallback for an unknown explicit format', async () => {
|
|
256
|
+
const program = new Command();
|
|
257
|
+
const siteCmd = program.command('gemini');
|
|
258
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
259
|
+
await program.parseAsync(['node', 'webcmd', 'gemini', 'ask', '--format', 'xml']);
|
|
260
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
261
|
+
expect(mockRenderOutput).toHaveBeenCalledWith([{ response: 'hello' }], expect.objectContaining({ fmt: 'xml', fmtExplicit: true }));
|
|
262
|
+
});
|
|
251
263
|
});
|
|
252
264
|
describe('commanderAdapter error envelope output', () => {
|
|
253
265
|
const cmd = {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PluginManifest } from './plugin-manifest.js';
|
|
2
|
+
export declare const COMMUNITY_PLUGINS_START = "<!-- webcmd-community-plugins:start -->";
|
|
3
|
+
export declare const COMMUNITY_PLUGINS_END = "<!-- webcmd-community-plugins:end -->";
|
|
4
|
+
export interface CommunityPluginOutputs {
|
|
5
|
+
rootManifest: PluginManifest;
|
|
6
|
+
readme: string;
|
|
7
|
+
}
|
|
8
|
+
/** Build generated catalog outputs without writing them. */
|
|
9
|
+
export declare function buildCommunityPluginOutputs(repoRoot: string): CommunityPluginOutputs;
|
|
10
|
+
/** Build and write the root manifest and README catalog. */
|
|
11
|
+
export declare function writeCommunityPluginOutputs(repoRoot: string): CommunityPluginOutputs;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { validatePluginAuthor, } from './plugin-manifest.js';
|
|
4
|
+
export const COMMUNITY_PLUGINS_START = '<!-- webcmd-community-plugins:start -->';
|
|
5
|
+
export const COMMUNITY_PLUGINS_END = '<!-- webcmd-community-plugins:end -->';
|
|
6
|
+
/** Build generated catalog outputs without writing them. */
|
|
7
|
+
export function buildCommunityPluginOutputs(repoRoot) {
|
|
8
|
+
const rootManifestPath = path.join(repoRoot, 'webcmd-plugin.json');
|
|
9
|
+
const rootManifest = readObject(rootManifestPath, 'webcmd-plugin.json');
|
|
10
|
+
const plugins = readRepositoryPlugins(repoRoot);
|
|
11
|
+
const readmePath = path.join(repoRoot, 'README.md');
|
|
12
|
+
const readme = fs.readFileSync(readmePath, 'utf-8');
|
|
13
|
+
const marketplaces = readExternalMarketplaces(repoRoot);
|
|
14
|
+
return {
|
|
15
|
+
rootManifest: { ...rootManifest, plugins },
|
|
16
|
+
readme: replaceGeneratedSection(readme, renderCommunityPlugins(plugins, marketplaces)),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Build and write the root manifest and README catalog. */
|
|
20
|
+
export function writeCommunityPluginOutputs(repoRoot) {
|
|
21
|
+
const generated = buildCommunityPluginOutputs(repoRoot);
|
|
22
|
+
fs.writeFileSync(path.join(repoRoot, 'webcmd-plugin.json'), `${JSON.stringify(generated.rootManifest, null, 2)}\n`);
|
|
23
|
+
fs.writeFileSync(path.join(repoRoot, 'README.md'), generated.readme);
|
|
24
|
+
return generated;
|
|
25
|
+
}
|
|
26
|
+
function readRepositoryPlugins(repoRoot) {
|
|
27
|
+
const pluginsDir = path.join(repoRoot, 'plugins');
|
|
28
|
+
const directoryNames = fs.readdirSync(pluginsDir, { withFileTypes: true })
|
|
29
|
+
.filter((entry) => entry.isDirectory())
|
|
30
|
+
.map((entry) => entry.name)
|
|
31
|
+
.sort((a, b) => a.localeCompare(b));
|
|
32
|
+
const plugins = {};
|
|
33
|
+
for (const directoryName of directoryNames) {
|
|
34
|
+
const relativeManifestPath = `plugins/${directoryName}/webcmd-plugin.json`;
|
|
35
|
+
const manifestPath = path.join(repoRoot, relativeManifestPath);
|
|
36
|
+
const manifest = readObject(manifestPath, relativeManifestPath);
|
|
37
|
+
const name = requiredString(manifest, 'name', relativeManifestPath);
|
|
38
|
+
if (name !== directoryName) {
|
|
39
|
+
throw new Error(`${relativeManifestPath}: name must match directory "${directoryName}"`);
|
|
40
|
+
}
|
|
41
|
+
let author;
|
|
42
|
+
try {
|
|
43
|
+
author = validatePluginAuthor(manifest.author);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
throw new Error(`${relativeManifestPath}: ${errorMessage(err)}`);
|
|
47
|
+
}
|
|
48
|
+
plugins[name] = {
|
|
49
|
+
path: `plugins/${directoryName}`,
|
|
50
|
+
version: requiredString(manifest, 'version', relativeManifestPath),
|
|
51
|
+
description: requiredString(manifest, 'description', relativeManifestPath),
|
|
52
|
+
webcmd: requiredString(manifest, 'webcmd', relativeManifestPath),
|
|
53
|
+
author,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return plugins;
|
|
57
|
+
}
|
|
58
|
+
function readExternalMarketplaces(repoRoot) {
|
|
59
|
+
const catalog = readObject(path.join(repoRoot, 'plugin-catalog.json'), 'plugin-catalog.json');
|
|
60
|
+
if (!Array.isArray(catalog.sources)) {
|
|
61
|
+
throw new Error('plugin-catalog.json: sources must be an array');
|
|
62
|
+
}
|
|
63
|
+
return catalog.sources.flatMap((source, index) => {
|
|
64
|
+
if (!isObject(source) || typeof source.source !== 'string') {
|
|
65
|
+
throw new Error(`plugin-catalog.json: sources[${index}].source must be a string`);
|
|
66
|
+
}
|
|
67
|
+
const match = /^github:([\w.-]+)\/([\w.-]+)$/.exec(source.source);
|
|
68
|
+
if (!match)
|
|
69
|
+
return [];
|
|
70
|
+
const repository = `${match[1]}/${match[2]}`;
|
|
71
|
+
return repository === 'agentrhq/webcmd' ? [] : [repository];
|
|
72
|
+
}).sort((a, b) => a.localeCompare(b));
|
|
73
|
+
}
|
|
74
|
+
function renderCommunityPlugins(plugins, marketplaces) {
|
|
75
|
+
const lines = [
|
|
76
|
+
COMMUNITY_PLUGINS_START,
|
|
77
|
+
'### Community plugins',
|
|
78
|
+
'',
|
|
79
|
+
'| Plugin | Description | Author |',
|
|
80
|
+
'| --- | --- | --- |',
|
|
81
|
+
];
|
|
82
|
+
for (const [name, plugin] of Object.entries(plugins)) {
|
|
83
|
+
const author = plugin.author;
|
|
84
|
+
lines.push(`| [\`${markdownCell(name)}\`](./plugins/${name}/) | ${markdownCell(plugin.description)} | [${markdownCell(author.name)}](https://github.com/${author.handle}) |`);
|
|
85
|
+
}
|
|
86
|
+
if (marketplaces.length > 0) {
|
|
87
|
+
lines.push('', '### Plugin marketplaces', '', '| Marketplace |', '| --- |');
|
|
88
|
+
for (const marketplace of marketplaces) {
|
|
89
|
+
lines.push(`| [${markdownCell(marketplace)}](https://github.com/${marketplace}) |`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push(COMMUNITY_PLUGINS_END);
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
function replaceGeneratedSection(readme, section) {
|
|
96
|
+
const start = readme.indexOf(COMMUNITY_PLUGINS_START);
|
|
97
|
+
const end = readme.indexOf(COMMUNITY_PLUGINS_END);
|
|
98
|
+
if (start < 0 || end < start) {
|
|
99
|
+
throw new Error(`README.md: expected ${COMMUNITY_PLUGINS_START} and ${COMMUNITY_PLUGINS_END}`);
|
|
100
|
+
}
|
|
101
|
+
if (readme.indexOf(COMMUNITY_PLUGINS_START, start + COMMUNITY_PLUGINS_START.length) >= 0
|
|
102
|
+
|| readme.indexOf(COMMUNITY_PLUGINS_END, end + COMMUNITY_PLUGINS_END.length) >= 0) {
|
|
103
|
+
throw new Error('README.md: community plugin markers must appear exactly once');
|
|
104
|
+
}
|
|
105
|
+
return `${readme.slice(0, start)}${section}${readme.slice(end + COMMUNITY_PLUGINS_END.length)}`;
|
|
106
|
+
}
|
|
107
|
+
function readObject(filePath, label) {
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
throw new Error(`${label}: malformed JSON (${errorMessage(err)})`);
|
|
114
|
+
}
|
|
115
|
+
if (!isObject(parsed))
|
|
116
|
+
throw new Error(`${label}: expected an object`);
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
function requiredString(value, key, label) {
|
|
120
|
+
const result = typeof value[key] === 'string' ? value[key].trim() : '';
|
|
121
|
+
if (!result)
|
|
122
|
+
throw new Error(`${label}: ${key} must be a non-empty string`);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
function markdownCell(value) {
|
|
126
|
+
return value
|
|
127
|
+
.replace(/\\/g, '\\\\')
|
|
128
|
+
.replace(/\|/g, '\\|')
|
|
129
|
+
.replace(/\[/g, '\\[')
|
|
130
|
+
.replace(/\]/g, '\\]')
|
|
131
|
+
.replace(/\r?\n/g, ' ');
|
|
132
|
+
}
|
|
133
|
+
function isObject(value) {
|
|
134
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
135
|
+
}
|
|
136
|
+
function errorMessage(err) {
|
|
137
|
+
return err instanceof Error ? err.message : String(err);
|
|
138
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { buildCommunityPluginOutputs, writeCommunityPluginOutputs } from './community-plugin-sync.js';
|
|
6
|
+
const START_MARKER = '<!-- webcmd-community-plugins:start -->';
|
|
7
|
+
const END_MARKER = '<!-- webcmd-community-plugins:end -->';
|
|
8
|
+
describe('community plugin sync', () => {
|
|
9
|
+
let repoRoot;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-plugin-generator-'));
|
|
12
|
+
writeJson('webcmd-plugin.json', {
|
|
13
|
+
name: 'webcmd',
|
|
14
|
+
version: '0.2.0',
|
|
15
|
+
description: 'Webcmd plugin collection',
|
|
16
|
+
webcmd: '>=0.2.0',
|
|
17
|
+
plugins: { stale: { path: 'plugins/stale' } },
|
|
18
|
+
});
|
|
19
|
+
writeJson('plugin-catalog.json', {
|
|
20
|
+
version: 1,
|
|
21
|
+
sources: [
|
|
22
|
+
{
|
|
23
|
+
id: 'agentrhq/webcmd',
|
|
24
|
+
source: 'github:agentrhq/webcmd',
|
|
25
|
+
manifestUrl: 'https://raw.githubusercontent.com/agentrhq/webcmd/main/webcmd-plugin.json',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'other/plugins',
|
|
29
|
+
source: 'github:other/plugins',
|
|
30
|
+
manifestUrl: 'https://raw.githubusercontent.com/other/plugins/main/webcmd-plugin.json',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
fs.writeFileSync(path.join(repoRoot, 'README.md'), [
|
|
35
|
+
'# Webcmd',
|
|
36
|
+
'',
|
|
37
|
+
'## Plugins',
|
|
38
|
+
'',
|
|
39
|
+
START_MARKER,
|
|
40
|
+
'old generated content',
|
|
41
|
+
END_MARKER,
|
|
42
|
+
'',
|
|
43
|
+
'## Writing adapters',
|
|
44
|
+
'',
|
|
45
|
+
].join('\n'));
|
|
46
|
+
writePlugin('zeta', {
|
|
47
|
+
name: 'zeta',
|
|
48
|
+
version: '0.1.0',
|
|
49
|
+
description: 'Last plugin',
|
|
50
|
+
webcmd: '>=0.2.0',
|
|
51
|
+
author: { name: 'Zed', handle: 'zed-user' },
|
|
52
|
+
});
|
|
53
|
+
writePlugin('alpha', {
|
|
54
|
+
name: 'alpha',
|
|
55
|
+
version: '1.0.0',
|
|
56
|
+
description: 'Forecasts | alerts',
|
|
57
|
+
webcmd: '>=0.3.0',
|
|
58
|
+
author: { name: 'Alice', handle: 'alice' },
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
fs.rmSync(repoRoot, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
it('builds a sorted root manifest and three-column README catalog', () => {
|
|
65
|
+
const result = buildCommunityPluginOutputs(repoRoot);
|
|
66
|
+
expect(Object.keys(result.rootManifest.plugins ?? {})).toEqual(['alpha', 'zeta']);
|
|
67
|
+
expect(result.rootManifest).toMatchObject({
|
|
68
|
+
name: 'webcmd',
|
|
69
|
+
version: '0.2.0',
|
|
70
|
+
plugins: {
|
|
71
|
+
alpha: {
|
|
72
|
+
path: 'plugins/alpha',
|
|
73
|
+
version: '1.0.0',
|
|
74
|
+
description: 'Forecasts | alerts',
|
|
75
|
+
webcmd: '>=0.3.0',
|
|
76
|
+
author: { name: 'Alice', handle: 'alice' },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
expect(result.readme).toContain('| Plugin | Description | Author |');
|
|
81
|
+
expect(result.readme).toContain('| [`alpha`](./plugins/alpha/) | Forecasts \\| alerts | [Alice](https://github.com/alice) |');
|
|
82
|
+
expect(result.readme.indexOf('`alpha`')).toBeLessThan(result.readme.indexOf('`zeta`'));
|
|
83
|
+
expect(result.readme).toContain('### Plugin marketplaces');
|
|
84
|
+
expect(result.readme).toContain('[other/plugins](https://github.com/other/plugins)');
|
|
85
|
+
expect(result.readme).not.toContain('[agentrhq/webcmd](https://github.com/agentrhq/webcmd)');
|
|
86
|
+
});
|
|
87
|
+
it('writes the generated files', () => {
|
|
88
|
+
const result = writeCommunityPluginOutputs(repoRoot);
|
|
89
|
+
expect(JSON.parse(fs.readFileSync(path.join(repoRoot, 'webcmd-plugin.json'), 'utf-8'))).toEqual(result.rootManifest);
|
|
90
|
+
expect(fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf-8')).toBe(result.readme);
|
|
91
|
+
});
|
|
92
|
+
it('fails with the plugin path when author metadata is missing', () => {
|
|
93
|
+
writePlugin('alpha', {
|
|
94
|
+
name: 'alpha',
|
|
95
|
+
version: '1.0.0',
|
|
96
|
+
description: 'Forecasts',
|
|
97
|
+
webcmd: '>=0.3.0',
|
|
98
|
+
});
|
|
99
|
+
expect(() => buildCommunityPluginOutputs(repoRoot)).toThrow('plugins/alpha/webcmd-plugin.json: Invalid author');
|
|
100
|
+
});
|
|
101
|
+
it('fails with the plugin path for malformed JSON', () => {
|
|
102
|
+
fs.writeFileSync(path.join(repoRoot, 'plugins/alpha/webcmd-plugin.json'), '{broken');
|
|
103
|
+
expect(() => buildCommunityPluginOutputs(repoRoot)).toThrow('plugins/alpha/webcmd-plugin.json: malformed JSON');
|
|
104
|
+
});
|
|
105
|
+
it('rejects directory and manifest name mismatches', () => {
|
|
106
|
+
writePlugin('alpha', {
|
|
107
|
+
name: 'different',
|
|
108
|
+
version: '1.0.0',
|
|
109
|
+
description: 'Forecasts',
|
|
110
|
+
webcmd: '>=0.3.0',
|
|
111
|
+
author: { name: 'Alice', handle: 'alice' },
|
|
112
|
+
});
|
|
113
|
+
expect(() => buildCommunityPluginOutputs(repoRoot)).toThrow('plugins/alpha/webcmd-plugin.json: name must match directory "alpha"');
|
|
114
|
+
});
|
|
115
|
+
function writeJson(relativePath, value) {
|
|
116
|
+
const filePath = path.join(repoRoot, relativePath);
|
|
117
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
118
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
119
|
+
}
|
|
120
|
+
function writePlugin(name, manifest) {
|
|
121
|
+
writeJson(`plugins/${name}/webcmd-plugin.json`, manifest);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -16,7 +16,7 @@ export declare function hasAllManifests(manifestPaths: string[]): boolean;
|
|
|
16
16
|
* bypassing full CLI discovery and adapter loading.
|
|
17
17
|
*/
|
|
18
18
|
export declare function getCompletionsFromManifest(words: string[], cursor: number, manifestPaths: string[]): string[];
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
*/
|
|
22
|
-
export declare function
|
|
19
|
+
/** Return a generated shell script without taking ownership of stdout. */
|
|
20
|
+
export declare function getCompletionScriptFast(shell: string): string | undefined;
|
|
21
|
+
/** Return a supported shell script or the canonical local completion error. */
|
|
22
|
+
export declare function requireCompletionScriptFast(shell: string): string;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* It only reads pre-compiled cli-manifest.json files synchronously.
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
|
+
import { getCommandCompletionCandidates } from './command-presentation.js';
|
|
9
|
+
import { CliError } from './errors.js';
|
|
8
10
|
import { BUILTIN_COMMANDS, bashCompletionScript, zshCompletionScript, fishCompletionScript, } from './completion-shared.js';
|
|
9
11
|
/**
|
|
10
12
|
* Returns true only if ALL manifest files exist and are readable.
|
|
@@ -29,32 +31,9 @@ export function hasAllManifests(manifestPaths) {
|
|
|
29
31
|
*/
|
|
30
32
|
export function getCompletionsFromManifest(words, cursor, manifestPaths) {
|
|
31
33
|
const entries = loadManifestEntries(manifestPaths);
|
|
32
|
-
if (entries === null)
|
|
34
|
+
if (entries === null)
|
|
33
35
|
return [];
|
|
34
|
-
|
|
35
|
-
if (cursor <= 1) {
|
|
36
|
-
const sites = new Set();
|
|
37
|
-
for (const entry of entries) {
|
|
38
|
-
sites.add(entry.site);
|
|
39
|
-
}
|
|
40
|
-
return [...BUILTIN_COMMANDS, ...sites].sort();
|
|
41
|
-
}
|
|
42
|
-
const site = words[0];
|
|
43
|
-
if (BUILTIN_COMMANDS.includes(site)) {
|
|
44
|
-
return [];
|
|
45
|
-
}
|
|
46
|
-
if (cursor === 2) {
|
|
47
|
-
const subcommands = [];
|
|
48
|
-
for (const entry of entries) {
|
|
49
|
-
if (entry.site === site) {
|
|
50
|
-
subcommands.push(entry.name);
|
|
51
|
-
if (entry.aliases?.length)
|
|
52
|
-
subcommands.push(...entry.aliases);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return [...new Set(subcommands)].sort();
|
|
56
|
-
}
|
|
57
|
-
return [];
|
|
36
|
+
return getCommandCompletionCandidates(entries, words, cursor, BUILTIN_COMMANDS);
|
|
58
37
|
}
|
|
59
38
|
// ── Shell script generators (re-exported from shared, no registry dependency) ───────
|
|
60
39
|
const SHELL_SCRIPTS = {
|
|
@@ -62,15 +41,17 @@ const SHELL_SCRIPTS = {
|
|
|
62
41
|
zsh: zshCompletionScript,
|
|
63
42
|
fish: fishCompletionScript,
|
|
64
43
|
};
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
44
|
+
/** Return a generated shell script without taking ownership of stdout. */
|
|
45
|
+
export function getCompletionScriptFast(shell) {
|
|
46
|
+
return SHELL_SCRIPTS[shell]?.();
|
|
47
|
+
}
|
|
48
|
+
/** Return a supported shell script or the canonical local completion error. */
|
|
49
|
+
export function requireCompletionScriptFast(shell) {
|
|
50
|
+
const script = getCompletionScriptFast(shell);
|
|
51
|
+
if (script === undefined) {
|
|
52
|
+
throw new CliError('UNSUPPORTED_SHELL', `Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
53
|
+
}
|
|
54
|
+
return script;
|
|
74
55
|
}
|
|
75
56
|
function loadManifestEntries(manifestPaths) {
|
|
76
57
|
const entries = [];
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
* This module MUST remain lightweight (no registry, no discovery imports).
|
|
5
5
|
* Both completion.ts (full path) and completion-fast.ts (manifest path) import from here.
|
|
6
6
|
*/
|
|
7
|
+
import type { RootHelpPresentation } from './command-presentation.js';
|
|
7
8
|
/**
|
|
8
9
|
* Built-in (non-dynamic) top-level commands.
|
|
9
10
|
*/
|
|
10
11
|
export declare const BUILTIN_COMMANDS: string[];
|
|
12
|
+
export declare const LOCAL_ONLY_COMMAND_HELP = "Run `webcmd setup` and choose local mode to use local-only commands.";
|
|
13
|
+
export declare const HOSTED_ROOT_HELP: RootHelpPresentation;
|
|
14
|
+
export declare const HOSTED_BUILTIN_COMMANDS: string[];
|
|
11
15
|
export declare function bashCompletionScript(): string;
|
|
12
16
|
export declare function zshCompletionScript(): string;
|
|
13
17
|
export declare function fishCompletionScript(): string;
|
|
@@ -20,6 +20,44 @@ export const BUILTIN_COMMANDS = [
|
|
|
20
20
|
'external',
|
|
21
21
|
'completion',
|
|
22
22
|
];
|
|
23
|
+
export const LOCAL_ONLY_COMMAND_HELP = 'Run `webcmd setup` and choose local mode to use local-only commands.';
|
|
24
|
+
export const HOSTED_ROOT_HELP = {
|
|
25
|
+
description: 'Make any website your CLI. Zero setup. AI-powered.',
|
|
26
|
+
usage: [
|
|
27
|
+
`${CLI_COMMAND} <site> <command> [args] [options]`,
|
|
28
|
+
`${CLI_COMMAND} browser <session> <command> [args] [options]`,
|
|
29
|
+
`${CLI_COMMAND} list [options]`,
|
|
30
|
+
`${CLI_COMMAND} setup`,
|
|
31
|
+
],
|
|
32
|
+
options: [
|
|
33
|
+
{ flags: '--profile <name>', description: 'Browser profile/context alias for browser runtime commands' },
|
|
34
|
+
{ flags: '-V, --version', description: 'Output the version number' },
|
|
35
|
+
{ flags: '-h, --help', description: 'Display help for command' },
|
|
36
|
+
],
|
|
37
|
+
commands: [
|
|
38
|
+
{ name: 'browser', description: 'Browser control through a hosted browser session' },
|
|
39
|
+
{ name: 'completion <shell>', description: 'Output a shell completion script' },
|
|
40
|
+
{ name: 'list', description: 'List all available hosted CLI commands' },
|
|
41
|
+
{ name: 'setup', description: 'Configure local or hosted mode' },
|
|
42
|
+
],
|
|
43
|
+
localOnlyCommands: [
|
|
44
|
+
{ name: 'adapter', description: 'Manage adapters installed on this computer' },
|
|
45
|
+
{ name: 'antigravity', description: 'Run the local Antigravity proxy' },
|
|
46
|
+
{ name: 'auth', description: 'Inspect credentials in the local browser runtime' },
|
|
47
|
+
{ name: 'convention-audit', description: 'Audit adapter source files on this computer' },
|
|
48
|
+
{ name: 'daemon', description: 'Manage the local Webcmd daemon' },
|
|
49
|
+
{ name: 'doctor', description: 'Diagnose local browser bridge connectivity' },
|
|
50
|
+
{ name: 'external', description: 'Manage local CLI passthrough commands' },
|
|
51
|
+
{ name: 'plugin', description: 'Manage plugins installed on this computer' },
|
|
52
|
+
{ name: 'profile', description: 'Manage profiles in the local browser runtime' },
|
|
53
|
+
{ name: 'skills', description: 'Manage bundled skills on this computer' },
|
|
54
|
+
{ name: 'validate', description: 'Validate local CLI definitions' },
|
|
55
|
+
{ name: 'verify', description: 'Validate and smoke-test local adapters' },
|
|
56
|
+
],
|
|
57
|
+
localOnlyExplanation: LOCAL_ONLY_COMMAND_HELP,
|
|
58
|
+
};
|
|
59
|
+
export const HOSTED_BUILTIN_COMMANDS = HOSTED_ROOT_HELP.commands
|
|
60
|
+
.map((command) => command.name.split(/\s/, 1)[0]);
|
|
23
61
|
// ── Shell script generators ────────────────────────────────────────────────
|
|
24
62
|
export function bashCompletionScript() {
|
|
25
63
|
return `# Bash completion for ${CLI_COMMAND}
|
package/dist/src/completion.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
7
|
*/
|
|
8
8
|
import { getRegistry } from './registry.js';
|
|
9
|
+
import { getCommandCompletionCandidates, toPresentableCommand } from './command-presentation.js';
|
|
9
10
|
import { CliError } from './errors.js';
|
|
10
11
|
import { BUILTIN_COMMANDS, bashCompletionScript, zshCompletionScript, fishCompletionScript, } from './completion-shared.js';
|
|
11
12
|
// Re-export shell scripts so existing callers (cli.ts) don't break
|
|
@@ -19,33 +20,8 @@ export { bashCompletionScript, zshCompletionScript, fishCompletionScript };
|
|
|
19
20
|
* @param cursor - 1-based position of the word being completed (1 = first arg)
|
|
20
21
|
*/
|
|
21
22
|
export function getCompletions(words, cursor) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const sites = new Set();
|
|
25
|
-
for (const [, cmd] of getRegistry()) {
|
|
26
|
-
sites.add(cmd.site);
|
|
27
|
-
}
|
|
28
|
-
return [...BUILTIN_COMMANDS, ...sites].sort();
|
|
29
|
-
}
|
|
30
|
-
const site = words[0];
|
|
31
|
-
// If the first word is a built-in command, no further completion
|
|
32
|
-
if (BUILTIN_COMMANDS.includes(site)) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
// cursor === 2 → completing the sub-command name under a site
|
|
36
|
-
if (cursor === 2) {
|
|
37
|
-
const subcommands = [];
|
|
38
|
-
for (const [, cmd] of getRegistry()) {
|
|
39
|
-
if (cmd.site === site) {
|
|
40
|
-
subcommands.push(cmd.name);
|
|
41
|
-
if (cmd.aliases?.length)
|
|
42
|
-
subcommands.push(...cmd.aliases);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return [...new Set(subcommands)].sort();
|
|
46
|
-
}
|
|
47
|
-
// cursor >= 3 → no further completion
|
|
48
|
-
return [];
|
|
23
|
+
const commands = [...new Set(getRegistry().values())].map(toPresentableCommand);
|
|
24
|
+
return getCommandCompletionCandidates(commands, words, cursor, BUILTIN_COMMANDS);
|
|
49
25
|
}
|
|
50
26
|
// ── Shell script generators ────────────────────────────────────────────────
|
|
51
27
|
/**
|