tabctl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -0
- package/cli/lib/args.js +141 -0
- package/cli/lib/client.js +62 -0
- package/cli/lib/commands/index.js +45 -0
- package/cli/lib/commands/list.js +159 -0
- package/cli/lib/commands/meta.js +493 -0
- package/cli/lib/commands/params.js +378 -0
- package/cli/lib/commands/profile.js +91 -0
- package/cli/lib/constants.js +30 -0
- package/cli/lib/help.js +205 -0
- package/cli/lib/options.js +408 -0
- package/cli/lib/output.js +64 -0
- package/cli/lib/pagination.js +55 -0
- package/cli/lib/policy.js +91 -0
- package/cli/lib/report.js +61 -0
- package/cli/lib/scope.js +278 -0
- package/cli/lib/snapshot.js +216 -0
- package/cli/lib/types.js +2 -0
- package/cli/tabctl.js +841 -0
- package/extension/background.js +3372 -0
- package/extension/manifest.json +23 -0
- package/extension/manifest.template.json +22 -0
- package/host/host.js +428 -0
- package/host/host.sh +5 -0
- package/host/lib/undo.js +60 -0
- package/package.json +43 -0
- package/shared/config.js +111 -0
- package/shared/extension-sync.js +70 -0
- package/shared/profiles.js +78 -0
- package/shared/version.js +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# tabctl
|
|
2
|
+
|
|
3
|
+
`tabctl` is a command-line tool that gives you terminal control over your browser tabs. List, search, group, move, close, deduplicate, inspect, and report on tabs without leaving your terminal — across Chrome and Edge.
|
|
4
|
+
|
|
5
|
+
It works through a lightweight local stack: the CLI talks to a native messaging host, which proxies requests to a browser extension. A policy file can protect pinned tabs or specific groups from automated actions, and every mutation is undoable.
|
|
6
|
+
|
|
7
|
+
This repo contains:
|
|
8
|
+
- Chrome/Edge extension (tab/group inspection + actions)
|
|
9
|
+
- Native messaging host (Node)
|
|
10
|
+
- CLI (`tabctl`) for on-demand workflows
|
|
11
|
+
|
|
12
|
+
The host only runs while the browser is open and the extension is connected.
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### 1. Build and install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install
|
|
20
|
+
npm run build
|
|
21
|
+
npm link # puts tabctl on your PATH
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
If you haven't run `npm link`, you can always use `node ./cli/tabctl.js` instead of `tabctl`.
|
|
25
|
+
|
|
26
|
+
### 2. Set up your browser
|
|
27
|
+
|
|
28
|
+
Run the interactive setup — it syncs the extension, tells you where to load it, and prompts for the extension ID:
|
|
29
|
+
|
|
30
|
+
<!-- test: "setup interactive mode reads extension-id from stdin" -->
|
|
31
|
+
```bash
|
|
32
|
+
tabctl setup --browser chrome
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This will:
|
|
36
|
+
1. Copy the extension to a stable location (`~/.local/state/tabctl/extension/`)
|
|
37
|
+
2. Print the path (and copy it to your clipboard)
|
|
38
|
+
3. Ask you to load it as an unpacked extension in `chrome://extensions`
|
|
39
|
+
4. Prompt you to paste the extension ID
|
|
40
|
+
|
|
41
|
+
> **Edge?** Use `--browser edge` and load from `edge://extensions` instead.
|
|
42
|
+
|
|
43
|
+
If you already know your extension ID, skip the interactive flow:
|
|
44
|
+
|
|
45
|
+
<!-- test: "setup writes native host manifest for chrome" -->
|
|
46
|
+
```bash
|
|
47
|
+
tabctl setup --browser chrome --extension-id <your-extension-id>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Verify and explore
|
|
51
|
+
|
|
52
|
+
<!-- test: "ping sends ping action", "list sends list action" -->
|
|
53
|
+
```bash
|
|
54
|
+
tabctl ping # check the connection
|
|
55
|
+
tabctl list # see your open tabs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> **Multiple browsers?** See [Multi-Browser Setup](#multi-browser-setup) for running tabctl with both Chrome and Edge.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
<!-- test: "list sends list action", "analyze passes tab ids and github options", "inspect passes signal options", "close without confirm fails", "report format md returns markdown content", "undo sends undo action with txid" -->
|
|
63
|
+
| Command | Description |
|
|
64
|
+
|---------|-------------|
|
|
65
|
+
| `tabctl list` | List open tabs and groups |
|
|
66
|
+
| `tabctl analyze` | Find stale or duplicate tabs |
|
|
67
|
+
| `tabctl inspect --tab <id>` | Extract page metadata or CSS selectors |
|
|
68
|
+
| `tabctl close --tab <id>` | Close tabs with full undo support |
|
|
69
|
+
| `tabctl report` | Generate reports in JSON, Markdown, or CSV |
|
|
70
|
+
| `tabctl undo` | Revert the last action |
|
|
71
|
+
|
|
72
|
+
See [CLI.md](CLI.md) for the full command reference, options, and examples.
|
|
73
|
+
|
|
74
|
+
## Screenshot output
|
|
75
|
+
When `--out` is omitted, screenshots are written to `./.tabctl/screenshots/<timestamp>` and the JSON response includes `writtenTo`.
|
|
76
|
+
|
|
77
|
+
## Agent workflow (context -> selector)
|
|
78
|
+
Use screenshots only when you need visual context, then extract selectors with `inspect`.
|
|
79
|
+
|
|
80
|
+
1) Capture context (full page tiles):
|
|
81
|
+
<!-- test: "screenshot passes capture options" -->
|
|
82
|
+
```bash
|
|
83
|
+
tabctl screenshot --tab <id> --mode full
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
2) Identify the element visually, then extract its selector:
|
|
87
|
+
<!-- test: "inspect passes signal options" -->
|
|
88
|
+
```bash
|
|
89
|
+
tabctl inspect --tab <id> --signal selector --selector '{"name":"target","selector":".your-selector"}'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
3) If you need an absolute URL, set `--selector-attr href-url` or set `attr` to `href-url`/`src-url`:
|
|
93
|
+
<!-- test: "inspect passes selector attr" -->
|
|
94
|
+
```bash
|
|
95
|
+
tabctl inspect --tab <id> --signal selector --selector '{"name":"link","selector":"a[href]","attr":"href-url"}'
|
|
96
|
+
tabctl inspect --tab <id> --signal selector --selector "link=a[href]" --selector-attr href-url
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Agent skills
|
|
100
|
+
|
|
101
|
+
Install the tabctl skill for agents (OpenCode, Claude Code, Codex, etc.) via the bundled command (uses the Skills CLI under the hood):
|
|
102
|
+
|
|
103
|
+
<!-- test: "skill install creates project skill link" -->
|
|
104
|
+
```bash
|
|
105
|
+
tabctl skill
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This writes a project-local skill to `.opencode/skills/tabctl/SKILL.md`. You can also install globally:
|
|
109
|
+
|
|
110
|
+
<!-- test: "skill install supports global scope" -->
|
|
111
|
+
```bash
|
|
112
|
+
tabctl skill --global
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
To install into a specific agent toolchain with `skills`:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx skills add https://github.com/ekroon/tabctl --skill tabctl -a opencode
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Policy (protect tabs)
|
|
122
|
+
By default the CLI loads a policy file from:
|
|
123
|
+
`<configDir>/policy.json` (default: `~/.config/tabctl/policy.json`)
|
|
124
|
+
|
|
125
|
+
Set `TABCTL_CONFIG_DIR` to override the config directory.
|
|
126
|
+
|
|
127
|
+
This is a **protection-only** policy that marks tabs as ineligible for agent actions.
|
|
128
|
+
Example:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"protect": {
|
|
133
|
+
"pinned": true,
|
|
134
|
+
"groupTitles": ["🔒"]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Create a default policy file:
|
|
140
|
+
|
|
141
|
+
<!-- test: "policy init creates default file" -->
|
|
142
|
+
```bash
|
|
143
|
+
tabctl policy --init
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`tabctl setup` does not install a default policy.
|
|
147
|
+
See `config/policy.example.json` for a starter template.
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
Config directory: `TABCTL_CONFIG_DIR` → `$XDG_CONFIG_HOME/tabctl` → `~/.config/tabctl`
|
|
151
|
+
|
|
152
|
+
An optional `config.json` in the config directory can set `dataDir` to override where state files (socket, undo log) are stored. When `TABCTL_CONFIG_DIR` is set but no `dataDir` is configured, data defaults to `<configDir>/data/`; otherwise it uses `$XDG_STATE_HOME/tabctl` (or `~/.local/state/tabctl`).
|
|
153
|
+
|
|
154
|
+
See [CLI.md](CLI.md#configuration) for full details.
|
|
155
|
+
|
|
156
|
+
## Runtime state
|
|
157
|
+
- Socket: `<dataDir>/tabctl.sock` (default: `~/.local/state/tabctl/tabctl.sock`)
|
|
158
|
+
- Undo log: `<dataDir>/undo.jsonl` (default: `~/.local/state/tabctl/undo.jsonl`)
|
|
159
|
+
- Profile registry: `<configDir>/profiles.json`
|
|
160
|
+
|
|
161
|
+
## Multi-Browser Setup
|
|
162
|
+
|
|
163
|
+
> **Advanced topic** — you only need this if you run tabctl with more than one browser (e.g. Edge *and* Chrome).
|
|
164
|
+
|
|
165
|
+
tabctl supports multiple browser profiles. Each profile connects to a different **browser** (Chrome, Edge).
|
|
166
|
+
|
|
167
|
+
<!-- test: "setup writes native host manifest", "setup writes native host manifest for chrome", "setup --name creates custom-named profile", "profile-list with multiple profiles shows all", "profile-switch success updates default", "--profile flag overrides active profile" -->
|
|
168
|
+
```bash
|
|
169
|
+
# Setup for Edge
|
|
170
|
+
tabctl setup --browser edge --extension-id <edge-id>
|
|
171
|
+
|
|
172
|
+
# Setup for Chrome (with custom name)
|
|
173
|
+
tabctl setup --browser chrome --extension-id <chrome-id> --name chrome-work
|
|
174
|
+
|
|
175
|
+
# List profiles
|
|
176
|
+
tabctl profile-list
|
|
177
|
+
|
|
178
|
+
# Switch default
|
|
179
|
+
tabctl profile-switch edge
|
|
180
|
+
|
|
181
|
+
# One-off command with different profile
|
|
182
|
+
tabctl list --profile chrome-work
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Custom Chrome Profile Directories
|
|
186
|
+
|
|
187
|
+
If you launch Chrome with `--user-data-dir`, Chrome looks for native messaging manifests inside that directory. Use `--user-data-dir` in setup to write the manifest to the right place:
|
|
188
|
+
|
|
189
|
+
<!-- test: "setup --user-data-dir writes manifest to custom path" -->
|
|
190
|
+
```bash
|
|
191
|
+
tabctl setup --browser chrome --user-data-dir /path/to/chrome-profile
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This writes the manifest to `<user-data-dir>/NativeMessagingHosts/` instead of the system-wide location.
|
|
195
|
+
|
|
196
|
+
### How It Works
|
|
197
|
+
|
|
198
|
+
Each profile gets its own:
|
|
199
|
+
- Native host manifest and wrapper script
|
|
200
|
+
- Unix socket for CLI-host communication
|
|
201
|
+
- Undo history log
|
|
202
|
+
- Data directory
|
|
203
|
+
|
|
204
|
+
Policy is shared across all profiles.
|
|
205
|
+
|
|
206
|
+
## Security
|
|
207
|
+
- The native host is locked to your extension ID.
|
|
208
|
+
- All data stays local; no external API keys are used.
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
### TypeScript workflow
|
|
213
|
+
Source lives in `src/` and compiles to `build/`, then syncs to the runtime locations:
|
|
214
|
+
- `src/extension/background.ts` -> `extension/background.js`
|
|
215
|
+
- `src/host/host.ts` -> `host/host.js`
|
|
216
|
+
- `src/cli/tabctl.ts` -> `cli/tabctl.js`
|
|
217
|
+
- `src/tests/unit/*.ts` -> `tests/unit/*.js`
|
|
218
|
+
|
|
219
|
+
Build and test:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npm install
|
|
223
|
+
npm run build
|
|
224
|
+
npm test
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Versioning
|
|
228
|
+
The base version lives in `package.json` and is embedded into the CLI, host, and extension at build time.
|
|
229
|
+
|
|
230
|
+
Commands:
|
|
231
|
+
```bash
|
|
232
|
+
npm run bump:patch
|
|
233
|
+
npm run bump:minor
|
|
234
|
+
npm run bump:major
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Local builds default to a dev version when a `.git` directory is present, appending the short SHA.
|
|
238
|
+
```bash
|
|
239
|
+
npm run build
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
This produces versions like `0.1.0-dev.abc12345` (and appends `.dirty` when the repo has uncommitted changes).
|
|
243
|
+
|
|
244
|
+
For release builds without SHA, set:
|
|
245
|
+
```bash
|
|
246
|
+
TABCTL_VERSION_MODE=release npm run build
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Notes:
|
|
250
|
+
- `close --apply` uses the most recent analysis by `analysisId`.
|
|
251
|
+
- `close` without `--apply` requires `--confirm` to prevent accidental closure.
|
|
252
|
+
- Reports include short descriptions from page metadata and a fallback snippet.
|
|
253
|
+
- `list` and `group-list` paginate by default (limit 100); use `--limit`, `--offset`, or `--no-page`.
|
|
254
|
+
- Use `--group-id -1` or `--ungrouped` to target ungrouped tabs.
|
|
255
|
+
- `--selector` implies `--signal selector`.
|
|
256
|
+
- Unknown inspect signals are rejected (valid: `page-meta`, `github-state`, `selector`).
|
|
257
|
+
- Selector `attr` supports `href-url`/`src-url` to return absolute http(s) URLs.
|
|
258
|
+
- `screenshot --out` writes per-tab folders into the target directory.
|
|
259
|
+
- `tabctl undo` accepts a positional txid, `--txid`, or `--latest`.
|
|
260
|
+
- `tabctl history --json` returns a JSON array in `data`.
|
|
261
|
+
- `--format` is only supported by `report` (use `--json` elsewhere).
|
package/cli/lib/args.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeGroupColor = normalizeGroupColor;
|
|
4
|
+
exports.normalizeSignals = normalizeSignals;
|
|
5
|
+
exports.validateSignals = validateSignals;
|
|
6
|
+
exports.parseArgs = parseArgs;
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
const options_1 = require("./options");
|
|
9
|
+
const output_1 = require("./output");
|
|
10
|
+
function normalizeGroupColor(value) {
|
|
11
|
+
if (typeof value !== "string") {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const trimmed = value.trim().toLowerCase();
|
|
15
|
+
if (!trimmed) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
if (!constants_1.GROUP_COLORS.has(trimmed)) {
|
|
19
|
+
(0, output_1.errorOut)(`Invalid color: ${value}. Use one of: ${Array.from(constants_1.GROUP_COLORS).join(", ")}`);
|
|
20
|
+
}
|
|
21
|
+
return trimmed;
|
|
22
|
+
}
|
|
23
|
+
function normalizeSignals(value) {
|
|
24
|
+
if (!Array.isArray(value)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return value.map((signal) => String(signal).trim()).filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
function validateSignals(signals) {
|
|
30
|
+
for (const signal of signals) {
|
|
31
|
+
if (!constants_1.SUPPORTED_SIGNAL_SET.has(signal)) {
|
|
32
|
+
(0, output_1.errorOut)(`Unknown signal: ${signal}. Use one of: ${constants_1.SUPPORTED_SIGNALS.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function normalizeCommand(value) {
|
|
37
|
+
if (!value) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (value === "groups" || value === "group") {
|
|
41
|
+
return "group-list";
|
|
42
|
+
}
|
|
43
|
+
const meta = options_1.COMMANDS[value];
|
|
44
|
+
if (!meta?.aliases || meta.aliases.length === 0) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
return meta.aliases[0] ?? value;
|
|
48
|
+
}
|
|
49
|
+
function parseArgs(argv) {
|
|
50
|
+
const args = [...argv];
|
|
51
|
+
let command;
|
|
52
|
+
const options = { _: [] };
|
|
53
|
+
const warnings = [];
|
|
54
|
+
const pendingFlags = [];
|
|
55
|
+
const allowedFlags = (0, options_1.getAllowedFlags)();
|
|
56
|
+
const booleanFlags = (0, options_1.getBooleanFlags)();
|
|
57
|
+
while (args.length > 0) {
|
|
58
|
+
const arg = args.shift();
|
|
59
|
+
if (!arg.startsWith("--")) {
|
|
60
|
+
if (!command) {
|
|
61
|
+
command = normalizeCommand(arg);
|
|
62
|
+
if (command) {
|
|
63
|
+
const commandAllowedFlags = (0, options_1.getCommandAllowedFlags)(command);
|
|
64
|
+
for (const pending of pendingFlags) {
|
|
65
|
+
if (!commandAllowedFlags.has(pending)) {
|
|
66
|
+
warnings.push(`--${pending} is not supported by ${command}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
options._.push(arg);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const key = arg.slice(2);
|
|
76
|
+
if (!allowedFlags.has(key)) {
|
|
77
|
+
if (key === "format") {
|
|
78
|
+
(0, output_1.errorOut)("Unknown option: --format");
|
|
79
|
+
}
|
|
80
|
+
(0, output_1.errorOut)(`Unknown option: --${key}`);
|
|
81
|
+
}
|
|
82
|
+
if (command) {
|
|
83
|
+
const commandAllowedFlags = (0, options_1.getCommandAllowedFlags)(command);
|
|
84
|
+
if (!commandAllowedFlags.has(key)) {
|
|
85
|
+
warnings.push(`--${key} is not supported by ${command}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
pendingFlags.push(key);
|
|
90
|
+
}
|
|
91
|
+
// Boolean flags (no value needed)
|
|
92
|
+
if (booleanFlags.has(key)) {
|
|
93
|
+
options[key] = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Value required
|
|
97
|
+
const value = args.shift();
|
|
98
|
+
if (value == null) {
|
|
99
|
+
(0, output_1.errorOut)(`Missing value for --${key}`);
|
|
100
|
+
}
|
|
101
|
+
// Repeatable flags (accumulate into arrays)
|
|
102
|
+
if (key === "signal") {
|
|
103
|
+
if (!options.signal) {
|
|
104
|
+
options.signal = [];
|
|
105
|
+
}
|
|
106
|
+
options.signal.push(value);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (key === "tab") {
|
|
110
|
+
if (!options.tab) {
|
|
111
|
+
options.tab = [];
|
|
112
|
+
}
|
|
113
|
+
options.tab.push(value);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (key === "agent") {
|
|
117
|
+
if (!options.agent) {
|
|
118
|
+
options.agent = [];
|
|
119
|
+
}
|
|
120
|
+
options.agent.push(value);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (key === "url") {
|
|
124
|
+
if (!options.url) {
|
|
125
|
+
options.url = [];
|
|
126
|
+
}
|
|
127
|
+
options.url.push(value);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (key === "selector") {
|
|
131
|
+
if (!options.selector) {
|
|
132
|
+
options.selector = [];
|
|
133
|
+
}
|
|
134
|
+
options.selector.push(value);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// Single value flags
|
|
138
|
+
options[key] = value;
|
|
139
|
+
}
|
|
140
|
+
return { command, options, warnings };
|
|
141
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createRequestId = createRequestId;
|
|
7
|
+
exports.sendRequest = sendRequest;
|
|
8
|
+
exports.fetchSnapshot = fetchSnapshot;
|
|
9
|
+
const net_1 = __importDefault(require("net"));
|
|
10
|
+
const constants_1 = require("./constants");
|
|
11
|
+
function createRequestId() {
|
|
12
|
+
return `req-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
13
|
+
}
|
|
14
|
+
function sendRequest(payload, onProgress) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const { socketPath } = (0, constants_1.resolveConfig)();
|
|
17
|
+
const client = net_1.default.createConnection(socketPath);
|
|
18
|
+
let buffer = "";
|
|
19
|
+
client.on("connect", () => {
|
|
20
|
+
client.write(`${JSON.stringify(payload)}\n`);
|
|
21
|
+
});
|
|
22
|
+
client.on("data", (data) => {
|
|
23
|
+
buffer += data;
|
|
24
|
+
let index;
|
|
25
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
26
|
+
const line = buffer.slice(0, index).trim();
|
|
27
|
+
buffer = buffer.slice(index + 1);
|
|
28
|
+
if (!line) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = JSON.parse(line);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
client.end();
|
|
37
|
+
client.destroy();
|
|
38
|
+
reject(error);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (response.progress && onProgress) {
|
|
42
|
+
onProgress(response);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
client.end();
|
|
46
|
+
client.destroy();
|
|
47
|
+
resolve(response);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
client.on("error", (error) => {
|
|
52
|
+
reject(error);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function fetchSnapshot() {
|
|
57
|
+
const response = await sendRequest({ id: createRequestId(), action: "list", params: {} });
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return response.data;
|
|
62
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Command module index.
|
|
4
|
+
* Re-exports all command handlers and parameter builders.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.buildUndoParams = exports.buildHistoryParams = exports.buildScreenshotParams = exports.buildReportParams = exports.buildCloseParams = exports.buildArchiveParams = exports.buildMergeWindowParams = exports.buildMoveGroupParams = exports.buildMoveTabParams = exports.buildGroupAssignParams = exports.buildGroupUngroupParams = exports.buildGroupUpdateParams = exports.buildOpenParams = exports.buildRefreshParams = exports.buildFocusParams = exports.buildInspectParams = exports.buildAnalyzeParams = exports.runProfileRemove = exports.runProfileSwitch = exports.runProfileShow = exports.runProfileList = exports.runGroupList = exports.runList = exports.runPing = exports.runUndo = exports.runHistory = exports.runPolicy = exports.runVersion = exports.runSkillInstall = exports.runSetup = void 0;
|
|
8
|
+
// Meta commands (version, ping, setup, skill, policy, history, undo)
|
|
9
|
+
var meta_1 = require("./meta");
|
|
10
|
+
Object.defineProperty(exports, "runSetup", { enumerable: true, get: function () { return meta_1.runSetup; } });
|
|
11
|
+
Object.defineProperty(exports, "runSkillInstall", { enumerable: true, get: function () { return meta_1.runSkillInstall; } });
|
|
12
|
+
Object.defineProperty(exports, "runVersion", { enumerable: true, get: function () { return meta_1.runVersion; } });
|
|
13
|
+
Object.defineProperty(exports, "runPolicy", { enumerable: true, get: function () { return meta_1.runPolicy; } });
|
|
14
|
+
Object.defineProperty(exports, "runHistory", { enumerable: true, get: function () { return meta_1.runHistory; } });
|
|
15
|
+
Object.defineProperty(exports, "runUndo", { enumerable: true, get: function () { return meta_1.runUndo; } });
|
|
16
|
+
Object.defineProperty(exports, "runPing", { enumerable: true, get: function () { return meta_1.runPing; } });
|
|
17
|
+
// List commands (list, group-list)
|
|
18
|
+
var list_1 = require("./list");
|
|
19
|
+
Object.defineProperty(exports, "runList", { enumerable: true, get: function () { return list_1.runList; } });
|
|
20
|
+
Object.defineProperty(exports, "runGroupList", { enumerable: true, get: function () { return list_1.runGroupList; } });
|
|
21
|
+
// Profile commands (profile-list, profile-show, profile-switch, profile-remove)
|
|
22
|
+
var profile_1 = require("./profile");
|
|
23
|
+
Object.defineProperty(exports, "runProfileList", { enumerable: true, get: function () { return profile_1.runProfileList; } });
|
|
24
|
+
Object.defineProperty(exports, "runProfileShow", { enumerable: true, get: function () { return profile_1.runProfileShow; } });
|
|
25
|
+
Object.defineProperty(exports, "runProfileSwitch", { enumerable: true, get: function () { return profile_1.runProfileSwitch; } });
|
|
26
|
+
Object.defineProperty(exports, "runProfileRemove", { enumerable: true, get: function () { return profile_1.runProfileRemove; } });
|
|
27
|
+
// Parameter builders for all commands
|
|
28
|
+
var params_1 = require("./params");
|
|
29
|
+
Object.defineProperty(exports, "buildAnalyzeParams", { enumerable: true, get: function () { return params_1.buildAnalyzeParams; } });
|
|
30
|
+
Object.defineProperty(exports, "buildInspectParams", { enumerable: true, get: function () { return params_1.buildInspectParams; } });
|
|
31
|
+
Object.defineProperty(exports, "buildFocusParams", { enumerable: true, get: function () { return params_1.buildFocusParams; } });
|
|
32
|
+
Object.defineProperty(exports, "buildRefreshParams", { enumerable: true, get: function () { return params_1.buildRefreshParams; } });
|
|
33
|
+
Object.defineProperty(exports, "buildOpenParams", { enumerable: true, get: function () { return params_1.buildOpenParams; } });
|
|
34
|
+
Object.defineProperty(exports, "buildGroupUpdateParams", { enumerable: true, get: function () { return params_1.buildGroupUpdateParams; } });
|
|
35
|
+
Object.defineProperty(exports, "buildGroupUngroupParams", { enumerable: true, get: function () { return params_1.buildGroupUngroupParams; } });
|
|
36
|
+
Object.defineProperty(exports, "buildGroupAssignParams", { enumerable: true, get: function () { return params_1.buildGroupAssignParams; } });
|
|
37
|
+
Object.defineProperty(exports, "buildMoveTabParams", { enumerable: true, get: function () { return params_1.buildMoveTabParams; } });
|
|
38
|
+
Object.defineProperty(exports, "buildMoveGroupParams", { enumerable: true, get: function () { return params_1.buildMoveGroupParams; } });
|
|
39
|
+
Object.defineProperty(exports, "buildMergeWindowParams", { enumerable: true, get: function () { return params_1.buildMergeWindowParams; } });
|
|
40
|
+
Object.defineProperty(exports, "buildArchiveParams", { enumerable: true, get: function () { return params_1.buildArchiveParams; } });
|
|
41
|
+
Object.defineProperty(exports, "buildCloseParams", { enumerable: true, get: function () { return params_1.buildCloseParams; } });
|
|
42
|
+
Object.defineProperty(exports, "buildReportParams", { enumerable: true, get: function () { return params_1.buildReportParams; } });
|
|
43
|
+
Object.defineProperty(exports, "buildScreenshotParams", { enumerable: true, get: function () { return params_1.buildScreenshotParams; } });
|
|
44
|
+
Object.defineProperty(exports, "buildHistoryParams", { enumerable: true, get: function () { return params_1.buildHistoryParams; } });
|
|
45
|
+
Object.defineProperty(exports, "buildUndoParams", { enumerable: true, get: function () { return params_1.buildUndoParams; } });
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* List command handlers: list, group-list
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runList = runList;
|
|
7
|
+
exports.runGroupList = runGroupList;
|
|
8
|
+
const constants_1 = require("../constants");
|
|
9
|
+
const output_1 = require("../output");
|
|
10
|
+
const client_1 = require("../client");
|
|
11
|
+
const scope_1 = require("../scope");
|
|
12
|
+
const pagination_1 = require("../pagination");
|
|
13
|
+
const snapshot_1 = require("../snapshot");
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// List Command
|
|
16
|
+
// ============================================================================
|
|
17
|
+
async function runList(options, policyContext, policySummary, prettyOutput) {
|
|
18
|
+
// Validate scope flags early (before connecting) so invalid values
|
|
19
|
+
// are rejected without needing a socket connection.
|
|
20
|
+
const scope = (0, scope_1.resolveScopeFlags)(options);
|
|
21
|
+
const response = await (0, client_1.sendRequest)({
|
|
22
|
+
id: (0, client_1.createRequestId)(),
|
|
23
|
+
action: "list",
|
|
24
|
+
params: {},
|
|
25
|
+
client: {
|
|
26
|
+
component: "cli",
|
|
27
|
+
version: constants_1.VERSION,
|
|
28
|
+
baseVersion: constants_1.BASE_VERSION,
|
|
29
|
+
gitSha: constants_1.GIT_SHA,
|
|
30
|
+
dirty: constants_1.DIRTY,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
(0, output_1.printJson)(response, prettyOutput);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const data = response.data;
|
|
38
|
+
if (data && Array.isArray(data.windows)) {
|
|
39
|
+
const filtered = (0, snapshot_1.filterSnapshotByPolicy)(data, policyContext.policy);
|
|
40
|
+
if (typeof scope.windowId === "string") {
|
|
41
|
+
const resolvedWindowId = (0, scope_1.resolveWindowIdFromSnapshot)(filtered, scope.windowId);
|
|
42
|
+
scope.windowId = resolvedWindowId ?? null;
|
|
43
|
+
}
|
|
44
|
+
const allScope = options.all === true || !scope.hasScope;
|
|
45
|
+
const listParams = allScope
|
|
46
|
+
? { all: true }
|
|
47
|
+
: {
|
|
48
|
+
tabIds: scope.tabIds.length ? scope.tabIds : undefined,
|
|
49
|
+
groupTitle: scope.groupTitle || undefined,
|
|
50
|
+
groupId: scope.groupId != null ? scope.groupId : undefined,
|
|
51
|
+
windowId: scope.windowId != null ? scope.windowId : undefined,
|
|
52
|
+
};
|
|
53
|
+
if (typeof listParams.windowId === "string") {
|
|
54
|
+
const resolvedWindowId = (0, scope_1.resolveWindowIdFromSnapshot)(filtered, listParams.windowId);
|
|
55
|
+
listParams.windowId = resolvedWindowId ?? undefined;
|
|
56
|
+
}
|
|
57
|
+
const selection = (0, scope_1.selectTabsFromSnapshot)(filtered, listParams);
|
|
58
|
+
if (selection.error) {
|
|
59
|
+
(0, output_1.printJson)({ ok: false, error: selection.error }, prettyOutput);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const selectedTabs = selection.tabs || [];
|
|
63
|
+
const tabIdSet = new Set(selectedTabs.map((tab) => tab.tabId).filter((id) => typeof id === "number"));
|
|
64
|
+
const ordered = listParams.all
|
|
65
|
+
? (0, snapshot_1.orderTabs)(filtered, null)
|
|
66
|
+
: tabIdSet.size > 0
|
|
67
|
+
? (0, snapshot_1.orderTabs)(filtered, tabIdSet)
|
|
68
|
+
: [];
|
|
69
|
+
const scopeArgs = (0, scope_1.buildScopeArgs)(options, allScope);
|
|
70
|
+
const pagination = (0, pagination_1.resolvePagination)(options, ordered.length, "list", scopeArgs);
|
|
71
|
+
const start = pagination.offset;
|
|
72
|
+
const end = pagination.offset + pagination.limit;
|
|
73
|
+
const pagedTabs = ordered.slice(start, end);
|
|
74
|
+
const pagedSnapshot = (0, snapshot_1.buildPagedSnapshot)(filtered, pagedTabs);
|
|
75
|
+
data.windows = pagedSnapshot.windows;
|
|
76
|
+
if (pagination.page) {
|
|
77
|
+
data.page = pagination.page;
|
|
78
|
+
}
|
|
79
|
+
data.policy = policySummary;
|
|
80
|
+
}
|
|
81
|
+
else if (response.ok) {
|
|
82
|
+
response.policy = policySummary;
|
|
83
|
+
}
|
|
84
|
+
(0, output_1.emitVersionWarnings)(response, "list");
|
|
85
|
+
(0, output_1.printJson)(response, prettyOutput);
|
|
86
|
+
}
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Group-List Command
|
|
89
|
+
// ============================================================================
|
|
90
|
+
async function runGroupList(options, policyContext, policySummary, prettyOutput) {
|
|
91
|
+
const params = {
|
|
92
|
+
windowId: options.all ? undefined : options.window,
|
|
93
|
+
};
|
|
94
|
+
const response = await (0, client_1.sendRequest)({
|
|
95
|
+
id: (0, client_1.createRequestId)(),
|
|
96
|
+
action: "group-list",
|
|
97
|
+
params,
|
|
98
|
+
client: {
|
|
99
|
+
component: "cli",
|
|
100
|
+
version: constants_1.VERSION,
|
|
101
|
+
baseVersion: constants_1.BASE_VERSION,
|
|
102
|
+
gitSha: constants_1.GIT_SHA,
|
|
103
|
+
dirty: constants_1.DIRTY,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
(0, output_1.printJson)(response, prettyOutput);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const data = response.data;
|
|
111
|
+
// Fallback to snapshot if groups missing
|
|
112
|
+
if (data && (!Array.isArray(data.groups) || data.groups === null)) {
|
|
113
|
+
const scope = (0, scope_1.resolveScopeFlags)(options);
|
|
114
|
+
const snapshot = await (0, client_1.fetchSnapshot)();
|
|
115
|
+
if (snapshot) {
|
|
116
|
+
const filteredSnapshot = (0, snapshot_1.filterSnapshotByPolicy)(snapshot, policyContext.policy);
|
|
117
|
+
if (typeof scope.windowId === "string") {
|
|
118
|
+
const resolvedWindowId = (0, scope_1.resolveWindowIdFromSnapshot)(filteredSnapshot, scope.windowId);
|
|
119
|
+
scope.windowId = resolvedWindowId ?? null;
|
|
120
|
+
}
|
|
121
|
+
const scopeWindow = typeof scope.windowId === "number" && Number.isFinite(scope.windowId) ? scope.windowId : null;
|
|
122
|
+
const groups = (0, snapshot_1.buildGroupsFromSnapshot)(filteredSnapshot, scopeWindow);
|
|
123
|
+
data.groups = (0, scope_1.filterGroupsByScope)(groups, scope, filteredSnapshot, snapshot_1.buildTabIndex);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
data.groups = [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Apply scope filtering and pagination
|
|
130
|
+
if (data && Array.isArray(data.groups)) {
|
|
131
|
+
let groups = data.groups;
|
|
132
|
+
const scope = (0, scope_1.resolveScopeFlags)(options);
|
|
133
|
+
const allScope = options.all === true || !scope.hasScope;
|
|
134
|
+
let snapshot = null;
|
|
135
|
+
if (!allScope && scope.tabIds.length > 0) {
|
|
136
|
+
snapshot = await (0, client_1.fetchSnapshot)();
|
|
137
|
+
if (!snapshot) {
|
|
138
|
+
(0, output_1.errorOut)("Failed to load tabs for group-list filtering");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!allScope) {
|
|
142
|
+
groups = (0, scope_1.filterGroupsByScope)(groups, scope, snapshot, snapshot_1.buildTabIndex);
|
|
143
|
+
}
|
|
144
|
+
const scopeArgs = (0, scope_1.buildScopeArgs)(options, allScope);
|
|
145
|
+
const pagination = (0, pagination_1.resolvePagination)(options, groups.length, "group-list", scopeArgs);
|
|
146
|
+
const start = pagination.offset;
|
|
147
|
+
const end = pagination.offset + pagination.limit;
|
|
148
|
+
data.groups = groups.slice(start, end);
|
|
149
|
+
if (pagination.page) {
|
|
150
|
+
data.page = pagination.page;
|
|
151
|
+
}
|
|
152
|
+
data.policy = policySummary;
|
|
153
|
+
}
|
|
154
|
+
else if (response.ok) {
|
|
155
|
+
response.policy = policySummary;
|
|
156
|
+
}
|
|
157
|
+
(0, output_1.emitVersionWarnings)(response, "group-list");
|
|
158
|
+
(0, output_1.printJson)(response, prettyOutput);
|
|
159
|
+
}
|