@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
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { resolveTargetJs } from './target-resolver.js';
|
|
2
|
+
import { boundingRectResolvedJs, resolveTargetJs } from './target-resolver.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a fake element usable by the generated boundingRectResolvedJs. Rects are
|
|
5
|
+
* {left,top,width,height}; getBoundingClientRect derives right/bottom.
|
|
6
|
+
*/
|
|
7
|
+
function makeEl(opts) {
|
|
8
|
+
const attrs = opts.attrs ?? {};
|
|
9
|
+
const el = {
|
|
10
|
+
nodeType: 1,
|
|
11
|
+
tagName: opts.tag.toUpperCase(),
|
|
12
|
+
_cursor: opts.cursor ?? 'auto',
|
|
13
|
+
onclick: opts.onclick ? () => { } : null,
|
|
14
|
+
parentElement: null,
|
|
15
|
+
children: [],
|
|
16
|
+
scrollIntoView() { },
|
|
17
|
+
getBoundingClientRect() {
|
|
18
|
+
const r = opts.rect;
|
|
19
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height, right: r.left + r.width, bottom: r.top + r.height };
|
|
20
|
+
},
|
|
21
|
+
getAttribute(k) { return k === 'role' ? (opts.role ?? null) : (attrs[k] ?? null); },
|
|
22
|
+
hasAttribute(k) { return k === 'role' ? opts.role != null : k in attrs; },
|
|
23
|
+
contains(other) {
|
|
24
|
+
if (other === el)
|
|
25
|
+
return true;
|
|
26
|
+
for (const c of el.children) {
|
|
27
|
+
if (c === other || (c.contains && c.contains(other)))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return el;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve computed cursor with CSS inheritance, so the fake DOM matches the
|
|
36
|
+
* browser: a child with no own cursor inherits its ancestors' pointer cursor. */
|
|
37
|
+
function inheritedCursor(node) {
|
|
38
|
+
let n = node;
|
|
39
|
+
while (n) {
|
|
40
|
+
if (n._cursor && n._cursor !== 'auto')
|
|
41
|
+
return n._cursor;
|
|
42
|
+
n = n.parentElement;
|
|
43
|
+
}
|
|
44
|
+
return 'auto';
|
|
45
|
+
}
|
|
46
|
+
/** Run the generated boundingRectResolvedJs against a fake DOM. */
|
|
47
|
+
function runRect(resolved, elementAt) {
|
|
48
|
+
const fakeWindow = { __resolved: resolved, getComputedStyle: (n) => ({ cursor: inheritedCursor(n) }) };
|
|
49
|
+
const fakeDoc = { elementFromPoint: (x, y) => elementAt(x, y) };
|
|
50
|
+
const js = boundingRectResolvedJs({ skipScroll: true, forClick: true });
|
|
51
|
+
// js is an IIFE expression `(() => {...})()`; arrow captures window/document
|
|
52
|
+
// params. Wrap in parens so ASI doesn't turn `return\n(` into `return;`.
|
|
53
|
+
return new Function('window', 'document', 'return (' + js + ')')(fakeWindow, fakeDoc);
|
|
54
|
+
}
|
|
55
|
+
describe('boundingRectResolvedJs runtime behavior', () => {
|
|
56
|
+
it('reports hit=target and no retarget when the centre lands on the element', () => {
|
|
57
|
+
const btn = makeEl({ tag: 'button', rect: { left: 0, top: 0, width: 100, height: 40 } });
|
|
58
|
+
const out = runRect(btn, () => btn);
|
|
59
|
+
expect(out.visible).toBe(true);
|
|
60
|
+
expect(out.hit).toBe('target');
|
|
61
|
+
expect(out.retargeted).toBe(false);
|
|
62
|
+
expect(out.x).toBe(50);
|
|
63
|
+
expect(out.y).toBe(20);
|
|
64
|
+
});
|
|
65
|
+
it('retargets an <svg> icon to its clickable <div> ancestor (#2071)', () => {
|
|
66
|
+
const div = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 80, height: 80 }, cursor: 'pointer' });
|
|
67
|
+
const svg = makeEl({ tag: 'svg', rect: { left: 20, top: 20, width: 40, height: 40 } });
|
|
68
|
+
svg.parentElement = div;
|
|
69
|
+
div.children = [svg];
|
|
70
|
+
// elementFromPoint at the div centre returns the svg (topmost paint), which
|
|
71
|
+
// is contained by div → hit=target on the retargeted element.
|
|
72
|
+
const out = runRect(svg, () => svg);
|
|
73
|
+
expect(out.retargeted).toBe(true);
|
|
74
|
+
expect(out.hit).toBe('target');
|
|
75
|
+
// measured rect is the div's (80x80 → centre 40,40), not the svg's
|
|
76
|
+
expect(out.w).toBe(80);
|
|
77
|
+
expect(out.x).toBe(40);
|
|
78
|
+
expect(out.y).toBe(40);
|
|
79
|
+
});
|
|
80
|
+
it('retargets to an ancestor that owns an onclick handler even without cursor:pointer (#2071)', () => {
|
|
81
|
+
const div = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 60, height: 60 }, onclick: true });
|
|
82
|
+
const svg = makeEl({ tag: 'svg', rect: { left: 10, top: 10, width: 40, height: 40 } });
|
|
83
|
+
svg.parentElement = div;
|
|
84
|
+
div.children = [svg];
|
|
85
|
+
const out = runRect(svg, () => svg);
|
|
86
|
+
expect(out.retargeted).toBe(true);
|
|
87
|
+
expect(out.w).toBe(60);
|
|
88
|
+
});
|
|
89
|
+
it('does not retarget when no ancestor is clickable', () => {
|
|
90
|
+
const wrap = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 60, height: 60 } });
|
|
91
|
+
const span = makeEl({ tag: 'span', rect: { left: 10, top: 10, width: 40, height: 40 } });
|
|
92
|
+
span.parentElement = wrap;
|
|
93
|
+
wrap.children = [span];
|
|
94
|
+
const out = runRect(span, () => span);
|
|
95
|
+
expect(out.retargeted).toBe(false);
|
|
96
|
+
expect(out.w).toBe(40);
|
|
97
|
+
});
|
|
98
|
+
it('treats an ancestor hit as trustworthy (open shadow-DOM host / own wrapper) and keeps the centre', () => {
|
|
99
|
+
// elementFromPoint returns a light-DOM ancestor (e.g. the shadow host) that
|
|
100
|
+
// contains the target — CDP pierces to the target, so this must NOT fall back.
|
|
101
|
+
const host = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 120, height: 60 } });
|
|
102
|
+
const btn = makeEl({ tag: 'button', rect: { left: 10, top: 10, width: 100, height: 40 } });
|
|
103
|
+
btn.parentElement = host;
|
|
104
|
+
host.children = [btn];
|
|
105
|
+
const out = runRect(btn, () => host); // topmost at every point is the host (ancestor)
|
|
106
|
+
expect(out.hit).toBe('ancestor');
|
|
107
|
+
expect(out.retargeted).toBe(false);
|
|
108
|
+
// centre kept — no wasted probing when the hit is already trustworthy
|
|
109
|
+
expect(out.x).toBe(60);
|
|
110
|
+
expect(out.y).toBe(30);
|
|
111
|
+
});
|
|
112
|
+
it('hover/dblClick mode (forClick omitted) returns the plain element centre with no hit-test', () => {
|
|
113
|
+
const div = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 80, height: 80 }, cursor: 'pointer' });
|
|
114
|
+
const svg = makeEl({ tag: 'svg', rect: { left: 20, top: 20, width: 40, height: 40 } });
|
|
115
|
+
svg.parentElement = div;
|
|
116
|
+
div.children = [svg];
|
|
117
|
+
const js = boundingRectResolvedJs({ skipScroll: true }); // no forClick
|
|
118
|
+
const out = new Function('window', 'document', 'return (' + js + ')')({ __resolved: svg, getComputedStyle: () => ({ cursor: 'pointer' }) }, { elementFromPoint: () => div });
|
|
119
|
+
// no retarget, no hit field — svg's own centre (40,40), unchanged behavior
|
|
120
|
+
expect(out.hit).toBeUndefined();
|
|
121
|
+
expect(out.retargeted).toBeUndefined();
|
|
122
|
+
expect(out.x).toBe(40);
|
|
123
|
+
expect(out.y).toBe(40);
|
|
124
|
+
expect(out.w).toBe(40);
|
|
125
|
+
});
|
|
126
|
+
it('reports hit=other when an overlay covers the element and no probe point lands (#2076)', () => {
|
|
127
|
+
const btn = makeEl({ tag: 'button', rect: { left: 0, top: 0, width: 100, height: 40 } });
|
|
128
|
+
const overlay = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 1000, height: 1000 } });
|
|
129
|
+
const out = runRect(btn, () => overlay); // every point hits the overlay
|
|
130
|
+
expect(out.hit).toBe('other');
|
|
131
|
+
expect(out.retargeted).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
it('recovers a hitting point by probing when the centre is occluded but a corner is clear (#2076)', () => {
|
|
134
|
+
const btn = makeEl({ tag: 'button', rect: { left: 0, top: 0, width: 100, height: 40 } });
|
|
135
|
+
const overlay = makeEl({ tag: 'div', rect: { left: 40, top: 15, width: 20, height: 10 } });
|
|
136
|
+
// Centre (50,20) is covered by the overlay; the top-left inset (3,3) is clear.
|
|
137
|
+
const out = runRect(btn, (x, y) => (x === 50 && y === 20 ? overlay : btn));
|
|
138
|
+
expect(out.hit).toBe('target');
|
|
139
|
+
// the returned point moved off the occluded centre (50,20)
|
|
140
|
+
expect(out.x === 50 && out.y === 20).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
3
143
|
/**
|
|
4
144
|
* Tests for the target resolver JS generator.
|
|
5
145
|
*
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* amount) to confirm an intentional removal.
|
|
23
23
|
*/
|
|
24
24
|
import type { ManifestEntry } from './manifest-types.js';
|
|
25
|
+
import { type HostedBrowserCommandContract } from './hosted/contract.js';
|
|
25
26
|
export type { ManifestEntry } from './manifest-types.js';
|
|
26
27
|
/**
|
|
27
28
|
* Thrown by `loadManifestEntries` when an adapter file looks like a CLI
|
|
@@ -66,6 +67,11 @@ export declare function loadManifestEntries(filePath: string, site: string, impo
|
|
|
66
67
|
export declare function scanClisDir(clisDir: string, importer?: (moduleHref: string) => Promise<unknown>): Promise<BuildManifestResult>;
|
|
67
68
|
export declare function buildManifest(): Promise<BuildManifestResult>;
|
|
68
69
|
export declare function serializeManifest(manifest: ManifestEntry[]): string;
|
|
70
|
+
export interface BuildManifestArtifacts {
|
|
71
|
+
manifestJson: string;
|
|
72
|
+
hostedContractJson: string;
|
|
73
|
+
}
|
|
74
|
+
export declare function buildManifestArtifacts(entries: ManifestEntry[], packageVersion: string, browserCatalog: readonly HostedBrowserCommandContract[]): BuildManifestArtifacts;
|
|
69
75
|
/**
|
|
70
76
|
* Metadata audit: every positional arg must carry a non-empty `help` string.
|
|
71
77
|
*
|
|
@@ -28,10 +28,12 @@ import { getErrorMessage } from './errors.js';
|
|
|
28
28
|
import { fullName, getRegistry } from './registry.js';
|
|
29
29
|
import { findPackageRoot, getCliManifestPath } from './package-paths.js';
|
|
30
30
|
import { isRecord } from './utils.js';
|
|
31
|
+
import { buildHostedContract, serializeHostedContract, } from './hosted/contract.js';
|
|
31
32
|
const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
|
|
32
33
|
const CLIS_DIR = path.join(PACKAGE_ROOT, 'clis');
|
|
33
34
|
// Write manifest next to clis/ so both dev and installed runtime can find it.
|
|
34
35
|
const OUTPUT = getCliManifestPath(CLIS_DIR);
|
|
36
|
+
const HOSTED_CONTRACT_OUTPUT = path.join(PACKAGE_ROOT, 'hosted-contract.json');
|
|
35
37
|
// Module is treated as a CLI command source if it either:
|
|
36
38
|
// 1. Calls `cli(...)` directly (the common case), or
|
|
37
39
|
// 2. Calls a factory `make<Pascal>Command(...)` from clis/_shared/ that
|
|
@@ -64,6 +66,7 @@ function toManifestArgs(args) {
|
|
|
64
66
|
positional: arg.positional || undefined,
|
|
65
67
|
help: arg.help ?? '',
|
|
66
68
|
choices: arg.choices,
|
|
69
|
+
file: arg.file,
|
|
67
70
|
}));
|
|
68
71
|
}
|
|
69
72
|
function toModulePath(filePath, site) {
|
|
@@ -211,6 +214,12 @@ export async function buildManifest() {
|
|
|
211
214
|
export function serializeManifest(manifest) {
|
|
212
215
|
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
213
216
|
}
|
|
217
|
+
export function buildManifestArtifacts(entries, packageVersion, browserCatalog) {
|
|
218
|
+
return {
|
|
219
|
+
manifestJson: serializeManifest(entries),
|
|
220
|
+
hostedContractJson: serializeHostedContract(buildHostedContract(entries, browserCatalog, packageVersion)),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
214
223
|
export function findManifestMetadataIssues(entries) {
|
|
215
224
|
const issues = [];
|
|
216
225
|
for (const entry of entries) {
|
|
@@ -330,9 +339,17 @@ async function main() {
|
|
|
330
339
|
process.exit(1);
|
|
331
340
|
}
|
|
332
341
|
}
|
|
342
|
+
const packagePath = path.resolve(PACKAGE_ROOT, 'package.json');
|
|
343
|
+
const packageMetadata = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
344
|
+
if (typeof packageMetadata.name !== 'string' || typeof packageMetadata.version !== 'string') {
|
|
345
|
+
throw new Error(`Package name and version are required in ${packagePath}`);
|
|
346
|
+
}
|
|
347
|
+
const artifacts = buildManifestArtifacts(entries, packageMetadata.version, []);
|
|
333
348
|
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
|
334
|
-
fs.writeFileSync(OUTPUT,
|
|
349
|
+
fs.writeFileSync(OUTPUT, artifacts.manifestJson);
|
|
350
|
+
fs.writeFileSync(HOSTED_CONTRACT_OUTPUT, artifacts.hostedContractJson);
|
|
335
351
|
console.log(`✅ Manifest compiled: ${entries.length} entries → ${OUTPUT}`);
|
|
352
|
+
console.log(`✅ Hosted contract compiled: ${packageMetadata.name}@${packageMetadata.version} → ${HOSTED_CONTRACT_OUTPUT}`);
|
|
336
353
|
// Restore executable permissions on bin entries.
|
|
337
354
|
// tsc does not preserve the +x bit, so after a clean rebuild the CLI
|
|
338
355
|
// entry-point loses its executable permission, causing "Permission denied".
|
|
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
5
|
import { cli, getRegistry, Strategy } from './registry.js';
|
|
6
|
-
import { ManifestImportError, diffRemovedEntries, findManifestMetadataIssues, loadManifestEntries, normalizeManifestPath, parseBuildManifestArgs, scanClisDir, serializeManifest, } from './build-manifest.js';
|
|
6
|
+
import { ManifestImportError, buildManifestArtifacts, diffRemovedEntries, findManifestMetadataIssues, loadManifestEntries, normalizeManifestPath, parseBuildManifestArgs, scanClisDir, serializeManifest, } from './build-manifest.js';
|
|
7
7
|
describe('manifest helper rules', () => {
|
|
8
8
|
const tempDirs = [];
|
|
9
9
|
afterEach(() => {
|
|
@@ -81,6 +81,62 @@ describe('manifest helper rules', () => {
|
|
|
81
81
|
expect(entries[0].sourceFile).not.toContain('\\');
|
|
82
82
|
getRegistry().delete(key);
|
|
83
83
|
});
|
|
84
|
+
it('preserves file metadata and builds both artifacts from one normalized snapshot', async () => {
|
|
85
|
+
const site = `manifest-files-${Date.now()}`;
|
|
86
|
+
const key = `${site}/upload`;
|
|
87
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-manifest-'));
|
|
88
|
+
tempDirs.push(dir);
|
|
89
|
+
const file = path.join(dir, `${site}.ts`);
|
|
90
|
+
fs.writeFileSync(file, `export const command = cli({ site: '${site}', name: 'upload', access: 'write' });`);
|
|
91
|
+
const entries = await loadManifestEntries(file, site, async () => ({
|
|
92
|
+
command: cli({
|
|
93
|
+
site,
|
|
94
|
+
name: 'upload',
|
|
95
|
+
access: 'write',
|
|
96
|
+
description: 'upload a document',
|
|
97
|
+
strategy: Strategy.PUBLIC,
|
|
98
|
+
browser: false,
|
|
99
|
+
args: [{
|
|
100
|
+
name: 'path',
|
|
101
|
+
type: 'str',
|
|
102
|
+
positional: true,
|
|
103
|
+
required: true,
|
|
104
|
+
help: 'Document path',
|
|
105
|
+
file: {
|
|
106
|
+
direction: 'input',
|
|
107
|
+
pathKind: 'file',
|
|
108
|
+
contentTypes: ['application/pdf'],
|
|
109
|
+
maxBytes: 5_000_000,
|
|
110
|
+
},
|
|
111
|
+
}],
|
|
112
|
+
}),
|
|
113
|
+
}));
|
|
114
|
+
expect(entries[0].args[0].file).toEqual({
|
|
115
|
+
direction: 'input',
|
|
116
|
+
pathKind: 'file',
|
|
117
|
+
contentTypes: ['application/pdf'],
|
|
118
|
+
maxBytes: 5_000_000,
|
|
119
|
+
});
|
|
120
|
+
const artifacts = buildManifestArtifacts(entries, '7.6.5', []);
|
|
121
|
+
expect(JSON.parse(artifacts.manifestJson)).toEqual(entries);
|
|
122
|
+
expect(JSON.parse(artifacts.hostedContractJson)).toMatchObject({
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
webcmdVersion: '7.6.5',
|
|
125
|
+
commands: [{
|
|
126
|
+
command: `${site}/upload`,
|
|
127
|
+
fileArguments: [{
|
|
128
|
+
name: 'path',
|
|
129
|
+
direction: 'input',
|
|
130
|
+
pathKind: 'file',
|
|
131
|
+
multiple: false,
|
|
132
|
+
required: true,
|
|
133
|
+
contentTypes: ['application/pdf'],
|
|
134
|
+
maxBytes: 5_000_000,
|
|
135
|
+
}],
|
|
136
|
+
}],
|
|
137
|
+
});
|
|
138
|
+
getRegistry().delete(key);
|
|
139
|
+
});
|
|
84
140
|
it('serializes freshPage for persistent browser commands', async () => {
|
|
85
141
|
const site = `manifest-fresh-page-${Date.now()}`;
|
|
86
142
|
const key = `${site}/checkout`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
export declare const LIST_COMMAND_DESCRIPTION = "List all available CLI commands";
|
|
3
|
+
export declare const LIST_FORMAT_DESCRIPTION = "Output format: table, json, yaml, md, csv";
|
|
4
|
+
export declare const COMPLETION_COMMAND_DESCRIPTION = "Output shell completion script";
|
|
5
|
+
export declare const COMPLETION_SHELL_DESCRIPTION = "Shell type: bash, zsh, or fish";
|
|
6
|
+
/** Configure built-in grammar shared by the local and hosted runtimes. */
|
|
7
|
+
export declare function configureListCommandSurface(command: Command): Command;
|
|
8
|
+
/** Configure completion grammar shared by the local and hosted runtimes. */
|
|
9
|
+
export declare function configureCompletionCommandSurface(command: Command): Command;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const LIST_COMMAND_DESCRIPTION = 'List all available CLI commands';
|
|
2
|
+
export const LIST_FORMAT_DESCRIPTION = 'Output format: table, json, yaml, md, csv';
|
|
3
|
+
export const COMPLETION_COMMAND_DESCRIPTION = 'Output shell completion script';
|
|
4
|
+
export const COMPLETION_SHELL_DESCRIPTION = 'Shell type: bash, zsh, or fish';
|
|
5
|
+
/** Configure built-in grammar shared by the local and hosted runtimes. */
|
|
6
|
+
export function configureListCommandSurface(command) {
|
|
7
|
+
return command
|
|
8
|
+
.description(LIST_COMMAND_DESCRIPTION)
|
|
9
|
+
.option('-f, --format <fmt>', LIST_FORMAT_DESCRIPTION, 'table');
|
|
10
|
+
}
|
|
11
|
+
/** Configure completion grammar shared by the local and hosted runtimes. */
|
|
12
|
+
export function configureCompletionCommandSurface(command) {
|
|
13
|
+
return command
|
|
14
|
+
.description(COMPLETION_COMMAND_DESCRIPTION)
|
|
15
|
+
.argument('<shell>', COMPLETION_SHELL_DESCRIPTION);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
8
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const checkerPath = path.join(packageRoot, 'scripts/check-hosted-contract.mjs');
|
|
10
|
+
const committedArtifactNames = ['cli-manifest.json'];
|
|
11
|
+
const fixtureRoots = [];
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const fixtureRoot of fixtureRoots.splice(0)) {
|
|
14
|
+
rmSync(fixtureRoot, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
function createCommittedArtifactFixture() {
|
|
18
|
+
const fixtureRoot = mkdtempSync(path.join(tmpdir(), 'webcmd-contract-committed-'));
|
|
19
|
+
fixtureRoots.push(fixtureRoot);
|
|
20
|
+
for (const artifactName of committedArtifactNames) {
|
|
21
|
+
copyFileSync(path.join(packageRoot, artifactName), path.join(fixtureRoot, artifactName));
|
|
22
|
+
}
|
|
23
|
+
return fixtureRoot;
|
|
24
|
+
}
|
|
25
|
+
function rootArtifactHashes() {
|
|
26
|
+
return Object.fromEntries(committedArtifactNames.map((artifactName) => [
|
|
27
|
+
artifactName,
|
|
28
|
+
createHash('sha256').update(readFileSync(path.join(packageRoot, artifactName))).digest('hex'),
|
|
29
|
+
]));
|
|
30
|
+
}
|
|
31
|
+
function runChecker(committedRoot) {
|
|
32
|
+
return spawnSync(process.execPath, [checkerPath], {
|
|
33
|
+
cwd: packageRoot,
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
env: { ...process.env, WEBCMD_CONTRACT_COMMITTED_ROOT: committedRoot },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
describe('hosted contract reproducibility checker', () => {
|
|
39
|
+
it('accepts a byte-identical committed manifest and generates the package contract', () => {
|
|
40
|
+
const result = runChecker(createCommittedArtifactFixture());
|
|
41
|
+
expect(result.status).toBe(0);
|
|
42
|
+
expect(result.stdout).toContain('hosted-contract.json generated successfully.');
|
|
43
|
+
}, 10_000);
|
|
44
|
+
it('rejects one stale byte without mutating root artifacts', () => {
|
|
45
|
+
const before = rootArtifactHashes();
|
|
46
|
+
const fixtureRoot = createCommittedArtifactFixture();
|
|
47
|
+
const stalePath = path.join(fixtureRoot, 'cli-manifest.json');
|
|
48
|
+
const staleBytes = readFileSync(stalePath);
|
|
49
|
+
staleBytes[0] ^= 1;
|
|
50
|
+
writeFileSync(stalePath, staleBytes);
|
|
51
|
+
const result = runChecker(fixtureRoot);
|
|
52
|
+
expect(result.status).toBe(1);
|
|
53
|
+
expect(result.stderr).toContain('cli-manifest.json');
|
|
54
|
+
expect(rootArtifactHashes()).toEqual(before);
|
|
55
|
+
}, 10_000);
|
|
56
|
+
});
|