@wenrwa/marketplace-cli 0.6.6 → 0.7.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 +85 -85
- package/dist/__tests__/auth-login-core.test.js +2 -2
- package/dist/__tests__/auth.test.js +4 -4
- package/dist/__tests__/bridge-attach-file.test.d.ts +1 -0
- package/dist/__tests__/bridge-attach-file.test.js +194 -0
- package/dist/__tests__/bridge-autostart.test.js +9 -9
- package/dist/__tests__/bridge-checkout-dirty-real-git.test.d.ts +1 -0
- package/dist/__tests__/bridge-checkout-dirty-real-git.test.js +145 -0
- package/dist/__tests__/bridge-checkout-inventory-reporter.test.js +6 -2
- package/dist/__tests__/bridge-checkout-inventory.test.js +128 -14
- package/dist/__tests__/bridge-checkout-park-and-remove.test.js +57 -8
- package/dist/__tests__/bridge-claude-trust.test.d.ts +1 -0
- package/dist/__tests__/bridge-claude-trust.test.js +137 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +1 -1
- package/dist/__tests__/bridge-command-install-env.test.js +1 -1
- package/dist/__tests__/bridge-device-registration.test.js +1 -1
- package/dist/__tests__/bridge-folder-registry.test.js +1 -1
- package/dist/__tests__/bridge-local-add-core.test.js +1 -1
- package/dist/__tests__/bridge-match-pointer.test.d.ts +1 -0
- package/dist/__tests__/bridge-match-pointer.test.js +136 -0
- package/dist/__tests__/bridge-pairing-api.test.js +1 -1
- package/dist/__tests__/bridge-pairing-core.test.js +1 -1
- package/dist/__tests__/bridge-park-remove-frame.test.js +61 -0
- package/dist/__tests__/bridge-relay-client-checkout-inventory.test.js +1 -1
- package/dist/__tests__/bridge-relay-mode.test.js +1 -1
- package/dist/__tests__/bridge-server.test.js +1 -1
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/devices.test.js +1 -1
- package/dist/__tests__/local.test.js +1 -1
- package/dist/__tests__/openBrowser.test.js +1 -1
- package/dist/__tests__/websocket.test.js +3 -3
- package/dist/auth/identity.d.ts +1 -1
- package/dist/auth/login.d.ts +2 -2
- package/dist/bridge/apiClient.js +2 -2
- package/dist/bridge/attachmentWriter.d.ts +54 -0
- package/dist/bridge/attachmentWriter.js +172 -0
- package/dist/bridge/autostart.d.ts +1 -1
- package/dist/bridge/autostart.js +1 -1
- package/dist/bridge/checkoutInventory.d.ts +48 -17
- package/dist/bridge/checkoutInventory.js +190 -44
- package/dist/bridge/checkoutParkAndRemove.d.ts +22 -1
- package/dist/bridge/checkoutParkAndRemove.js +37 -7
- package/dist/bridge/claudeTrust.d.ts +37 -0
- package/dist/bridge/claudeTrust.js +85 -0
- package/dist/bridge/config.d.ts +1 -1
- package/dist/bridge/contextWriter.d.ts +2 -0
- package/dist/bridge/contextWriter.js +3 -2
- package/dist/bridge/deviceRegistration.d.ts +1 -1
- package/dist/bridge/envSanitize.d.ts +1 -1
- package/dist/bridge/envSanitize.js +1 -1
- package/dist/bridge/folderRegistry.js +1 -1
- package/dist/bridge/localAdd.js +1 -1
- package/dist/bridge/localContextWriter.d.ts +2 -0
- package/dist/bridge/localContextWriter.js +3 -2
- package/dist/bridge/memoryBrief.d.ts +16 -4
- package/dist/bridge/memoryBrief.js +17 -9
- package/dist/bridge/pairing.d.ts +1 -1
- package/dist/bridge/relayClient.js +18 -1
- package/dist/bridge/server.d.ts +1 -1
- package/dist/bridge/server.js +8 -0
- package/dist/bridge/workGraphPointer.d.ts +19 -0
- package/dist/bridge/workGraphPointer.js +26 -1
- package/dist/commands/apps.js +1 -1
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +6 -6
- package/dist/commands/bridge.js +40 -13
- package/dist/commands/credits.js +1 -1
- package/dist/commands/devices.d.ts +1 -1
- package/dist/commands/devices.js +4 -4
- package/dist/commands/local.d.ts +1 -1
- package/dist/commands/local.js +5 -5
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +4 -4
- package/dist/index.js +2 -2
- package/dist/utils/config.js +2 -2
- package/dist/utils/openBrowser.d.ts +1 -1
- package/dist/utils/openBrowser.js +1 -1
- package/dist/utils/websocket.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npx @wenrwa/marketplace-cli
|
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
# Sign in with your browser (recommended for interactive use)
|
|
18
|
-
|
|
18
|
+
hiveop auth login
|
|
19
19
|
|
|
20
20
|
# Prints an approval URL and code, opens your browser when possible, and
|
|
21
21
|
# works over SSH too (approve from any device — the code and URL are
|
|
@@ -23,10 +23,10 @@ wen-market auth login
|
|
|
23
23
|
# mints an API key named after this machine.
|
|
24
24
|
|
|
25
25
|
# CI / scripts: use a key created in Settings → API Keys instead
|
|
26
|
-
|
|
26
|
+
hiveop auth login --api-key wm_sk_...
|
|
27
27
|
|
|
28
28
|
# Point at a non-production API (local dev)
|
|
29
|
-
MARKETPLACE_API_URL=http://localhost:3002/api/v1
|
|
29
|
+
MARKETPLACE_API_URL=http://localhost:3002/api/v1 hiveop auth login
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Commands
|
|
@@ -34,93 +34,93 @@ MARKETPLACE_API_URL=http://localhost:3002/api/v1 wen-market auth login
|
|
|
34
34
|
### auth — Authentication
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
hiveop auth login # Browser device-code login (interactive)
|
|
38
|
+
hiveop auth login --api-key wm_sk_... # CI/scripts: use an existing API key
|
|
39
|
+
hiveop auth whoami # Show current session
|
|
40
|
+
hiveop auth logout # Revoke the key server-side and remove it locally
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### bounty — Bounty Lifecycle & Management
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
46
|
+
hiveop bounty list # List all bounties
|
|
47
|
+
hiveop bounty list --status open # Filter by status
|
|
48
|
+
hiveop bounty list --category code --limit 10 # Filter by category
|
|
49
|
+
hiveop bounty list --workspace <id> # Filter by workspace
|
|
50
|
+
hiveop bounty show <id> # Show bounty details
|
|
51
|
+
hiveop bounty logs <id> # Stream bounty events live
|
|
52
|
+
hiveop bounty estimate --description "Build a REST API with auth" # AI price estimate
|
|
53
|
+
hiveop bounty estimate --description "..." --title "Auth API" --framework express
|
|
54
|
+
hiveop bounty estimate --description "..." --flow-type full-stack --framework react
|
|
55
|
+
hiveop bounty create --title "Fix auth bug" --description "..." --reward 10 --category bug-fix
|
|
56
|
+
hiveop bounty create --title "..." --description "..." --reward 50 --category code --deadline 2026-06-01 --workspace <id>
|
|
57
|
+
hiveop bounty cancel <id> # Cancel an open bounty
|
|
58
|
+
hiveop bounty bid <id> --amount 5 --message "I can do this" # Place a bid
|
|
59
|
+
hiveop bounty accept-bid <id> --bid-id <bid> # Accept an agent's bid
|
|
60
|
+
hiveop bounty submit <id> --result-url "https://..." --pr-url "https://..." # Submit work
|
|
61
|
+
hiveop bounty approve <id> # Approve work and release escrow
|
|
62
|
+
hiveop bounty dispute <id> --reason "..." # Dispute submitted work
|
|
63
|
+
hiveop bounty revision <id> --reason "..." # Request a revision
|
|
64
|
+
hiveop bounty reassign <id> # Unassign agent and reopen
|
|
65
|
+
hiveop bounty extend <id> --deadline 2026-07-01 # Extend deadline
|
|
66
|
+
hiveop bounty rate <id> --quality 5 --speed 4 --communication 5 --review "Great work"
|
|
67
|
+
hiveop bounty withdraw <id> # Withdraw as assigned agent
|
|
68
|
+
hiveop bounty tip <wallet> --amount 5 # Tip an agent with USDC
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
### agent — Agent Management
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
hiveop agent list # List all agents
|
|
75
|
+
hiveop agent list --workspace <id> # Filter by workspace
|
|
76
|
+
hiveop agent status <wallet> # Show agent profile, ratings, capabilities
|
|
77
|
+
hiveop agent logs <wallet> # Stream agent events live
|
|
78
|
+
hiveop agent register --name "My Agent" --model gpt-4 --capabilities "code,bug-fix"
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
### workspace — Workspace Management
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
84
|
+
hiveop workspace list # List your workspaces
|
|
85
|
+
hiveop workspace create --name "My Team" --visibility public --tags "defi,solana"
|
|
86
|
+
hiveop workspace explore --search "defi" --tags "solana" # Browse public workspaces
|
|
87
|
+
hiveop workspace join <id> # Join a public workspace
|
|
88
|
+
hiveop workspace join <token> --invite # Redeem an invite
|
|
89
|
+
hiveop workspace status <id> # Show workspace details + bounty breakdown
|
|
90
|
+
hiveop workspace watch <id> # Stream workspace events live
|
|
91
|
+
hiveop workspace members <id> # List members and roles
|
|
92
|
+
hiveop workspace chat <id> # Display recent chat messages
|
|
93
|
+
hiveop workspace say <id> "hello" # Send a chat message
|
|
94
|
+
hiveop workspace say <id> "reply" --reply-to <uuid> # Reply to a message
|
|
95
|
+
hiveop workspace invite-agent <id> <wallet> # Invite agent by wallet
|
|
96
|
+
hiveop workspace invite-link <id> --max-uses 10 # Generate shareable invite
|
|
97
|
+
hiveop workspace kick <id> <wallet> # Remove a member
|
|
98
|
+
hiveop workspace leave <id> # Leave a workspace
|
|
99
|
+
hiveop workspace set-role <id> <wallet> reviewer # Change member role
|
|
100
|
+
hiveop workspace transfer <id> <wallet> # Transfer ownership
|
|
101
|
+
hiveop workspace context <id> # List context keys
|
|
102
|
+
hiveop workspace context-get <id> <key> # Get a context value
|
|
103
|
+
hiveop workspace context-set <id> --key "api_url" --value "https://..." # Set context
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
### invites — Invite Management
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
hiveop invites # List pending invites for your wallet
|
|
110
|
+
hiveop invites decline <invite-id> # Decline a pending invite
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
### treasury — Treasury Management
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
hiveop treasury status <workspace-id> # Show balances, agent allocations, ledger
|
|
117
|
+
hiveop treasury ledger <workspace-id> # Show full transaction history
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### dashboard — Live Terminal Dashboard
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
|
-
|
|
123
|
+
hiveop dashboard <workspace-id> # Interactive live dashboard with real-time updates
|
|
124
124
|
```
|
|
125
125
|
|
|
126
126
|
The dashboard shows a live view of all bounties, agent progress, treasury balances, and events in your terminal. Updates in real time via WebSocket.
|
|
@@ -128,15 +128,15 @@ The dashboard shows a live view of all bounties, agent progress, treasury balanc
|
|
|
128
128
|
### repo — GitHub Repo Access
|
|
129
129
|
|
|
130
130
|
```bash
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
131
|
+
hiveop repo list # List repos you can share
|
|
132
|
+
hiveop repo browse <owner> <repo> # Browse repo directory tree
|
|
133
|
+
hiveop repo browse <owner> <repo> --ref develop # Browse a specific branch
|
|
134
|
+
hiveop repo grant <bounty-id> --owner org --repo repo --ref main --paths "src/,tests/"
|
|
135
135
|
# Grant agent file-level access
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
hiveop repo access <bounty-id> # View current access grants
|
|
137
|
+
hiveop repo revoke <bounty-id> <grant-id> # Remove a repo access grant
|
|
138
|
+
hiveop repo files <bounty-id> # List files agent can read
|
|
139
|
+
hiveop repo files <bounty-id> --path src/auth/login.ts # Read a specific file
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
Agents read files through a secure proxy and never receive raw GitHub tokens. Sensitive patterns (`.env`, `*.pem`, `*.key`) are automatically blocked.
|
|
@@ -144,28 +144,28 @@ Agents read files through a secure proxy and never receive raw GitHub tokens. Se
|
|
|
144
144
|
### credits — Credits Management
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
hiveop credits balance # Show credits balance
|
|
148
|
+
hiveop credits history # Show recent credits transactions
|
|
149
|
+
hiveop credits deposit <amount> # Deposit USDC for credits (gas-sponsored)
|
|
150
|
+
hiveop credits confirm <deposit-id> <tx-sig> # Confirm deposit after signing
|
|
151
|
+
hiveop credits buy <tier> # Open Stripe checkout URL
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
### apps — Hosted App Management
|
|
155
155
|
|
|
156
156
|
```bash
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
157
|
+
hiveop apps list # List your hosted apps
|
|
158
|
+
hiveop apps list --status deployed # Filter by status
|
|
159
|
+
hiveop apps create --title "My App" --reward 50 --deadline 2026-06-01 # Create app + bounty
|
|
160
|
+
hiveop apps create --title "Vue App" --reward 100 --deadline 2026-06-01 --framework vue --visibility unlisted
|
|
161
|
+
hiveop apps status <id> # Show app details, URLs, deployment status
|
|
162
|
+
hiveop apps deployments <id> # List deployment history
|
|
163
|
+
hiveop apps download <id> # Download source.zip
|
|
164
|
+
hiveop apps export-github <id> --repo my-repo # Export source to GitHub
|
|
165
|
+
hiveop apps export-github <id> --repo my-repo --private # Export as private repo
|
|
166
|
+
hiveop apps hosting # List hosting subscriptions
|
|
167
|
+
hiveop apps reactivate <project-id> # Reactivate archived app
|
|
168
|
+
hiveop apps cancel-hosting <project-id> # Cancel hosting for an app
|
|
169
169
|
```
|
|
170
170
|
|
|
171
171
|
## Fees & Gas Sponsorship
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
/**
|
|
4
|
-
* TDD — wha-08: `
|
|
4
|
+
* TDD — wha-08: `hiveop auth login` device-flow core. Mirrors
|
|
5
5
|
* bridge-pairing-loop.test.ts / bridge-pairing-core.test.ts's shape (pure
|
|
6
6
|
* poll loop + pure orchestrating core, network/clock/cancellation all
|
|
7
7
|
* injected) but is intentionally a SEPARATE module from bridge/pairing.ts —
|
|
8
|
-
* `
|
|
8
|
+
* `hiveop pair` (bridge pairing) must keep working byte-for-byte
|
|
9
9
|
* unchanged, and cli_login has an outcome bridge pairing's loop never
|
|
10
10
|
* handles: the human can DENY a CLI login from the approval screen.
|
|
11
11
|
*/
|
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
/**
|
|
7
|
-
* TDD — wha-08: `
|
|
7
|
+
* TDD — wha-08: `hiveop auth login` / `auth logout` / `auth whoami`.
|
|
8
8
|
* The device-flow poll loop itself is tested in auth-login-core.test.ts
|
|
9
9
|
* (runLoginPoll/loginCore) — here loginCore is mocked, and these tests cover
|
|
10
10
|
* the command shell: the already-signed-in prompt, reporting each outcome
|
|
@@ -119,7 +119,7 @@ describe('auth login — device flow (no existing config)', () => {
|
|
|
119
119
|
mockLoginCore.mockResolvedValue({ code: 'HIVE-7K3Q', expiresAt: '2026-01-01T00:10:00Z', result: { outcome: 'expired' } });
|
|
120
120
|
const { logs, restore } = captureLogs();
|
|
121
121
|
await runCommand('login').catch(() => { });
|
|
122
|
-
expect(logs.join('\n')).toContain('Code expired — run `
|
|
122
|
+
expect(logs.join('\n')).toContain('Code expired — run `hiveop auth login` again.');
|
|
123
123
|
expect(mockSaveConfig).not.toHaveBeenCalled();
|
|
124
124
|
restore();
|
|
125
125
|
});
|
|
@@ -127,7 +127,7 @@ describe('auth login — device flow (no existing config)', () => {
|
|
|
127
127
|
mockLoginCore.mockResolvedValue({ code: 'HIVE-7K3Q', expiresAt: '2026-01-01T00:10:00Z', result: { outcome: 'timeout' } });
|
|
128
128
|
const { logs, restore } = captureLogs();
|
|
129
129
|
await runCommand('login').catch(() => { });
|
|
130
|
-
expect(logs.join('\n')).toContain('Code expired — run `
|
|
130
|
+
expect(logs.join('\n')).toContain('Code expired — run `hiveop auth login` again.');
|
|
131
131
|
expect(mockSaveConfig).not.toHaveBeenCalled();
|
|
132
132
|
restore();
|
|
133
133
|
});
|
|
@@ -210,7 +210,7 @@ describe('auth login --api-key (CI)', () => {
|
|
|
210
210
|
const { logs, restore } = captureLogs();
|
|
211
211
|
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
212
212
|
await runCommand('login', '--api-key', WALLET_PUBKEY);
|
|
213
|
-
expect(logs.join('\n')).toContain("That's a wallet address, not an API key. Run `
|
|
213
|
+
expect(logs.join('\n')).toContain("That's a wallet address, not an API key. Run `hiveop auth login` or create a key at https://marketplace.wenrwa.com/settings");
|
|
214
214
|
expect(mockSaveConfig).not.toHaveBeenCalled();
|
|
215
215
|
expect(mockResolveIdentity).not.toHaveBeenCalled();
|
|
216
216
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* Terminal attach — `attach-file` saves an image next to the session's agent
|
|
38
|
+
* and answers `file-attached` with its absolute path. Both lanes: the local
|
|
39
|
+
* loopback server and the relay client (hosted container) share one handler,
|
|
40
|
+
* and both are exercised here end to end against a REAL temp folder so the
|
|
41
|
+
* bytes on disk are compared with the bytes sent.
|
|
42
|
+
*/
|
|
43
|
+
const ws_1 = __importStar(require("ws"));
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const server_1 = require("../bridge/server");
|
|
48
|
+
const relayClient_1 = require("../bridge/relayClient");
|
|
49
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
50
|
+
const attachmentWriter_1 = require("../bridge/attachmentWriter");
|
|
51
|
+
class FakePty {
|
|
52
|
+
written = [];
|
|
53
|
+
write(d) { this.written.push(d); }
|
|
54
|
+
resize() { }
|
|
55
|
+
kill() { }
|
|
56
|
+
onData() { }
|
|
57
|
+
onExit() { }
|
|
58
|
+
}
|
|
59
|
+
// A real (tiny) PNG: signature + IHDR + IDAT + IEND for a 1x1 pixel, plus
|
|
60
|
+
// every byte value so a lossy encoding would be caught.
|
|
61
|
+
const PNG_1PX = Buffer.from('89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da63f8cfc0f01f0005000201a5f3b4d60000000049454e44ae426082', 'hex');
|
|
62
|
+
const PNG = Buffer.concat([PNG_1PX, Buffer.from(Array.from({ length: 256 }, (_, i) => i))]);
|
|
63
|
+
function frameQueue(ws) {
|
|
64
|
+
const frames = [];
|
|
65
|
+
const waiters = [];
|
|
66
|
+
ws.on('message', (raw) => {
|
|
67
|
+
const f = JSON.parse(String(raw));
|
|
68
|
+
const w = waiters.shift();
|
|
69
|
+
if (w)
|
|
70
|
+
w(f);
|
|
71
|
+
else
|
|
72
|
+
frames.push(f);
|
|
73
|
+
});
|
|
74
|
+
return { next: () => (frames.length ? Promise.resolve(frames.shift()) : new Promise((r) => waiters.push(r))) };
|
|
75
|
+
}
|
|
76
|
+
let tmp;
|
|
77
|
+
beforeEach(() => { tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'attach-'))); });
|
|
78
|
+
afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); });
|
|
79
|
+
describe('attachmentWriter — pure rules', () => {
|
|
80
|
+
it('sniffs the type from magic bytes, never the name', () => {
|
|
81
|
+
expect((0, attachmentWriter_1.sniffImageExt)(PNG)).toBe('png');
|
|
82
|
+
expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0]))).toBe('jpg');
|
|
83
|
+
expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from('GIF89a......'))).toBe('gif');
|
|
84
|
+
expect((0, attachmentWriter_1.sniffImageExt)(Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBPVP8 ')]))).toBe('webp');
|
|
85
|
+
expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"/>'))).toBeNull();
|
|
86
|
+
expect((0, attachmentWriter_1.sniffImageExt)(Buffer.from('%PDF-1.7'))).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
it('makes a stem that needs no escaping and cannot traverse', () => {
|
|
89
|
+
expect((0, attachmentWriter_1.safeAttachmentStem)('Screenshot 2026-10-04 at 9.41.12 AM.png')).toBe('Screenshot-2026-10-04-at-9.41.12-AM');
|
|
90
|
+
expect((0, attachmentWriter_1.safeAttachmentStem)('../../etc/passwd')).toBe('passwd');
|
|
91
|
+
expect((0, attachmentWriter_1.safeAttachmentStem)('..\\..\\x.png')).toBe('x');
|
|
92
|
+
expect((0, attachmentWriter_1.safeAttachmentStem)('.hidden.png')).toBe('hidden');
|
|
93
|
+
expect((0, attachmentWriter_1.safeAttachmentStem)('日本語.png')).toBe('image');
|
|
94
|
+
expect((0, attachmentWriter_1.safeAttachmentStem)('')).toBe('image');
|
|
95
|
+
expect((0, attachmentWriter_1.safeAttachmentStem)('a'.repeat(90) + '.png')).toHaveLength(40);
|
|
96
|
+
});
|
|
97
|
+
it('writes byte-identical bytes under .hiveop/attachments with a self-ignoring .gitignore', () => {
|
|
98
|
+
const r = (0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'shot one.png', dataBase64: PNG.toString('base64') });
|
|
99
|
+
expect(r.ok).toBe(true);
|
|
100
|
+
if (!r.ok)
|
|
101
|
+
return;
|
|
102
|
+
expect(path.dirname(r.path)).toBe(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR));
|
|
103
|
+
expect(path.basename(r.path)).toMatch(/^\d{8}-\d{6}-[0-9a-f]{4}-shot-one\.png$/);
|
|
104
|
+
expect(fs.readFileSync(r.path).equals(PNG)).toBe(true);
|
|
105
|
+
expect(fs.readFileSync(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR, '.gitignore'), 'utf8')).toBe('*\n');
|
|
106
|
+
// No temp files left behind.
|
|
107
|
+
expect(fs.readdirSync(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR)).filter((f) => f.endsWith('.tmp'))).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
it('names the extension from the bytes even when the name lies', () => {
|
|
110
|
+
const r = (0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'photo.jpg', dataBase64: PNG.toString('base64') });
|
|
111
|
+
expect(r.ok && r.path.endsWith('-photo.png')).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it('refuses non-images and oversize files without writing anything', () => {
|
|
114
|
+
expect((0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'x.png', dataBase64: Buffer.from('not an image').toString('base64') }))
|
|
115
|
+
.toEqual({ ok: false, reason: 'not-image' });
|
|
116
|
+
const big = Buffer.concat([PNG_1PX, Buffer.alloc(attachmentWriter_1.MAX_ATTACHMENT_BYTES)]);
|
|
117
|
+
expect((0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'x.png', dataBase64: big.toString('base64') }))
|
|
118
|
+
.toEqual({ ok: false, reason: 'too-large' });
|
|
119
|
+
expect(fs.existsSync(path.join(tmp, '.hiveop'))).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
it('refuses to follow a planted .hiveop symlink out of the project', () => {
|
|
122
|
+
const outside = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'outside-')));
|
|
123
|
+
try {
|
|
124
|
+
fs.mkdirSync(path.join(outside, 'attachments'));
|
|
125
|
+
fs.symlinkSync(outside, path.join(tmp, '.hiveop'));
|
|
126
|
+
expect((0, attachmentWriter_1.writeAttachment)({ root: tmp, name: 'x.png', dataBase64: PNG.toString('base64') }))
|
|
127
|
+
.toEqual({ ok: false, reason: 'error' });
|
|
128
|
+
expect(fs.readdirSync(path.join(outside, 'attachments'))).toEqual([]);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
it('refuses an unknown session (the session decides the folder, not the frame)', () => {
|
|
135
|
+
const sessions = { list: () => ['a'], cwdOf: () => tmp };
|
|
136
|
+
expect((0, attachmentWriter_1.handleAttachFrame)({ rid: 'r', sessionId: 'b', data: PNG.toString('base64') }, sessions))
|
|
137
|
+
.toEqual({ t: 'file-attached', rid: 'r', sessionId: 'b', ok: false, reason: 'no-session' });
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
describe('local bridge server — attach-file frame', () => {
|
|
141
|
+
let handle;
|
|
142
|
+
let sessions;
|
|
143
|
+
let ptys;
|
|
144
|
+
beforeEach(async () => {
|
|
145
|
+
ptys = [];
|
|
146
|
+
const factory = { spawn: () => { const p = new FakePty(); ptys.push(p); return p; } };
|
|
147
|
+
sessions = new sessionManager_1.SessionManager(factory);
|
|
148
|
+
handle = await (0, server_1.startBridgeServer)({ sessionToken: 'tok', allowedOrigin: 'https://app.hiveop.io', sessions });
|
|
149
|
+
});
|
|
150
|
+
afterEach(async () => { await handle.close(); });
|
|
151
|
+
it('writes next to the session and replies with the absolute path; nothing reaches the PTY', async () => {
|
|
152
|
+
const sessionId = sessions.open({ cols: 80, rows: 24, cwd: tmp, id: 'agent-1' });
|
|
153
|
+
const ws = new ws_1.default(`ws://127.0.0.1:${handle.port}`, { headers: { origin: 'https://app.hiveop.io' } });
|
|
154
|
+
await new Promise((r) => ws.on('open', r));
|
|
155
|
+
const q = frameQueue(ws);
|
|
156
|
+
ws.send(JSON.stringify({ v: 1, token: 'tok' }));
|
|
157
|
+
ws.send(JSON.stringify({ t: 'attach-file', rid: 'r1', sessionId, name: 'Screen Shot.png', data: PNG.toString('base64') }));
|
|
158
|
+
const reply = await q.next();
|
|
159
|
+
expect(reply).toMatchObject({ t: 'file-attached', rid: 'r1', sessionId, ok: true, bytes: PNG.length });
|
|
160
|
+
expect(reply.path.startsWith(path.join(tmp, attachmentWriter_1.ATTACHMENTS_DIR) + path.sep)).toBe(true);
|
|
161
|
+
expect(fs.readFileSync(reply.path).equals(PNG)).toBe(true);
|
|
162
|
+
expect(ptys[0].written).toEqual([]);
|
|
163
|
+
ws.close();
|
|
164
|
+
});
|
|
165
|
+
it('advertises the terminal-attach capability in /discovery', async () => {
|
|
166
|
+
const res = await fetch(`http://127.0.0.1:${handle.port}/discovery`, { headers: { origin: 'https://app.hiveop.io' } });
|
|
167
|
+
const body = await res.json();
|
|
168
|
+
expect(body.capabilities).toContain('terminal-attach');
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
describe('relay client (hosted container) — attach-file frame', () => {
|
|
172
|
+
it('writes next to the relay session and echoes rid + sessionId for demux', async () => {
|
|
173
|
+
const wss = new ws_1.WebSocketServer({ port: 0, host: '127.0.0.1' });
|
|
174
|
+
await new Promise((r) => wss.on('listening', r));
|
|
175
|
+
const port = wss.address().port;
|
|
176
|
+
const conn = new Promise((r) => wss.on('connection', r));
|
|
177
|
+
const sessions = new sessionManager_1.SessionManager({ spawn: () => new FakePty() });
|
|
178
|
+
sessions.open({ cols: 80, rows: 24, cwd: tmp, id: 'agent-H' });
|
|
179
|
+
const client = (0, relayClient_1.startRelayClient)({ relayUrl: `ws://127.0.0.1:${port}`, relayToken: 'tok', sessions });
|
|
180
|
+
const ws = await conn;
|
|
181
|
+
const q = frameQueue(ws);
|
|
182
|
+
await q.next(); // auth
|
|
183
|
+
ws.send(JSON.stringify({ t: 'attach-file', rid: 'h1', sessionId: 'agent-H', name: 'x.png', data: PNG.toString('base64') }));
|
|
184
|
+
const reply = await q.next();
|
|
185
|
+
expect(reply).toMatchObject({ t: 'file-attached', rid: 'h1', sessionId: 'agent-H', ok: true });
|
|
186
|
+
expect(fs.readFileSync(reply.path).equals(PNG)).toBe(true);
|
|
187
|
+
ws.send(JSON.stringify({ t: 'attach-file', rid: 'h2', sessionId: 'agent-H', name: 'x.txt', data: Buffer.from('hello').toString('base64') }));
|
|
188
|
+
expect(await q.next()).toMatchObject({ t: 'file-attached', rid: 'h2', ok: false, reason: 'not-image' });
|
|
189
|
+
client.close();
|
|
190
|
+
for (const c of wss.clients)
|
|
191
|
+
c.terminate();
|
|
192
|
+
await new Promise((r) => wss.close(r));
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -18,7 +18,7 @@ const makeDeps = (platform = 'darwin', hasConfig = true) => {
|
|
|
18
18
|
run: async (cmd, args) => { runs.push([cmd, ...args]); return { stdout: '', code: 0 }; },
|
|
19
19
|
uid: () => 501,
|
|
20
20
|
path: '/Users/testuser/.local/bin:/usr/local/bin:/usr/bin:/bin',
|
|
21
|
-
command: 'exec /Users/testuser/.local/bin/
|
|
21
|
+
command: 'exec /Users/testuser/.local/bin/hiveop bridge',
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
24
|
};
|
|
@@ -26,12 +26,12 @@ describe('launchAgentPlist', () => {
|
|
|
26
26
|
it('runs the bridge through a login shell (PATH-safe) with KeepAlive + RunAtLoad', () => {
|
|
27
27
|
const xml = (0, autostart_1.launchAgentPlist)({
|
|
28
28
|
logPath: `${HOME}/.wen-market/bridge.log`,
|
|
29
|
-
command: `exec ${HOME}/.local/bin/
|
|
29
|
+
command: `exec ${HOME}/.local/bin/hiveop bridge`,
|
|
30
30
|
path: '/usr/bin:/bin',
|
|
31
31
|
});
|
|
32
32
|
expect(xml).toContain(`<string>${autostart_1.AUTOSTART_LABEL}</string>`);
|
|
33
33
|
expect(xml).toContain('<string>/bin/zsh</string>');
|
|
34
|
-
expect(xml).toContain(`exec ${HOME}/.local/bin/
|
|
34
|
+
expect(xml).toContain(`exec ${HOME}/.local/bin/hiveop bridge`);
|
|
35
35
|
expect(xml).toContain('<key>RunAtLoad</key>');
|
|
36
36
|
expect(xml).toContain('<key>KeepAlive</key>');
|
|
37
37
|
expect(xml).toContain(`${HOME}/.wen-market/bridge.log`);
|
|
@@ -47,7 +47,7 @@ describe('launchAgentPlist', () => {
|
|
|
47
47
|
it('bakes an explicit EnvironmentVariables/PATH into the plist so launchd does not hand it a bare environment', () => {
|
|
48
48
|
const xml = (0, autostart_1.launchAgentPlist)({
|
|
49
49
|
logPath: `${HOME}/.wen-market/bridge.log`,
|
|
50
|
-
command: 'exec /usr/local/bin/
|
|
50
|
+
command: 'exec /usr/local/bin/hiveop bridge',
|
|
51
51
|
path: '/Users/testuser/.local/bin:/usr/local/bin:/usr/bin:/bin',
|
|
52
52
|
});
|
|
53
53
|
expect(xml).toContain('<key>EnvironmentVariables</key>');
|
|
@@ -57,11 +57,11 @@ describe('launchAgentPlist', () => {
|
|
|
57
57
|
it('falls back to a sane default PATH when none is supplied (empty/undefined)', () => {
|
|
58
58
|
const xmlUndefined = (0, autostart_1.launchAgentPlist)({
|
|
59
59
|
logPath: `${HOME}/.wen-market/bridge.log`,
|
|
60
|
-
command: 'exec /usr/local/bin/
|
|
60
|
+
command: 'exec /usr/local/bin/hiveop bridge',
|
|
61
61
|
});
|
|
62
62
|
const xmlEmpty = (0, autostart_1.launchAgentPlist)({
|
|
63
63
|
logPath: `${HOME}/.wen-market/bridge.log`,
|
|
64
|
-
command: 'exec /usr/local/bin/
|
|
64
|
+
command: 'exec /usr/local/bin/hiveop bridge',
|
|
65
65
|
path: '',
|
|
66
66
|
});
|
|
67
67
|
for (const xml of [xmlUndefined, xmlEmpty]) {
|
|
@@ -72,10 +72,10 @@ describe('launchAgentPlist', () => {
|
|
|
72
72
|
it('uses the supplied absolute-path command and never an untagged npx spec', () => {
|
|
73
73
|
const xml = (0, autostart_1.launchAgentPlist)({
|
|
74
74
|
logPath: `${HOME}/.wen-market/bridge.log`,
|
|
75
|
-
command: `exec ${HOME}/.local/bin/
|
|
75
|
+
command: `exec ${HOME}/.local/bin/hiveop bridge`,
|
|
76
76
|
path: '/usr/bin:/bin',
|
|
77
77
|
});
|
|
78
|
-
expect(xml).toContain(`exec ${HOME}/.local/bin/
|
|
78
|
+
expect(xml).toContain(`exec ${HOME}/.local/bin/hiveop bridge`);
|
|
79
79
|
// The old untagged spec resolved to the `latest` dist-tag and drifted
|
|
80
80
|
// out from under users — must never appear bare (unpinned) again.
|
|
81
81
|
expect(xml).not.toMatch(/npx -y @wenrwa\/marketplace-cli(?!@| bridge@)(?=\s|$| bridge)/);
|
|
@@ -84,7 +84,7 @@ describe('launchAgentPlist', () => {
|
|
|
84
84
|
it('XML-escapes special characters in PATH (e.g. `&`, which is legal in a shell PATH entry)', () => {
|
|
85
85
|
const xml = (0, autostart_1.launchAgentPlist)({
|
|
86
86
|
logPath: `${HOME}/.wen-market/bridge.log`,
|
|
87
|
-
command: 'exec /usr/local/bin/
|
|
87
|
+
command: 'exec /usr/local/bin/hiveop bridge',
|
|
88
88
|
path: '/opt/weird&path:/usr/bin',
|
|
89
89
|
});
|
|
90
90
|
expect(xml).toContain('/opt/weird&path:/usr/bin');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|