@dmsdc-ai/aigentry-telepty 0.1.6 → 0.1.8
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 +11 -0
- package/cli.js +40 -48
- package/install.js +96 -64
- package/install.ps1 +1 -0
- package/install.sh +1 -0
- package/interactive-terminal.js +54 -0
- package/package.json +4 -4
- package/skill-installer.js +269 -0
- package/skills/telepty/SKILL.md +86 -0
- package/.deliberation_request.json +0 -1
- package/.deliberation_request2.json +0 -1
- package/.deliberation_request3.json +0 -1
- package/.gemini/skills/telepty/SKILL.md +0 -55
- package/.github/workflows/test-install.yml +0 -26
- package/aigentry-telepty-0.0.4.tgz +0 -0
- package/clipboard_image.png +0 -0
- package/test/auth.test.js +0 -56
- package/test/cli.test.js +0 -57
- package/test/daemon.test.js +0 -415
- package/test-pty.js +0 -14
- package/test-support/daemon-harness.js +0 -313
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const prompts = require('prompts');
|
|
7
|
+
|
|
8
|
+
const TARGET_CLIENTS = {
|
|
9
|
+
claude: {
|
|
10
|
+
label: 'Claude Code',
|
|
11
|
+
globalDir: () => path.join(os.homedir(), '.claude', 'skills'),
|
|
12
|
+
projectDir: (cwd) => path.join(cwd, '.claude', 'skills'),
|
|
13
|
+
defaultScope: 'global'
|
|
14
|
+
},
|
|
15
|
+
codex: {
|
|
16
|
+
label: 'Codex',
|
|
17
|
+
globalDir: () => path.join(os.homedir(), '.codex', 'skills'),
|
|
18
|
+
projectDir: (cwd) => path.join(cwd, '.codex', 'skills'),
|
|
19
|
+
defaultScope: 'global'
|
|
20
|
+
},
|
|
21
|
+
gemini: {
|
|
22
|
+
label: 'Gemini',
|
|
23
|
+
globalDir: () => path.join(os.homedir(), '.gemini', 'skills'),
|
|
24
|
+
projectDir: (cwd) => path.join(cwd, '.gemini', 'skills'),
|
|
25
|
+
defaultScope: 'project'
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resolveSkillsSourceRoot(packageRoot = __dirname) {
|
|
30
|
+
return path.join(packageRoot, 'skills');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function listPackagedSkills(packageRoot = __dirname) {
|
|
34
|
+
const sourceRoot = resolveSkillsSourceRoot(packageRoot);
|
|
35
|
+
if (!fs.existsSync(sourceRoot)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return fs.readdirSync(sourceRoot)
|
|
40
|
+
.filter((name) => {
|
|
41
|
+
const skillDir = path.join(sourceRoot, name);
|
|
42
|
+
return fs.statSync(skillDir).isDirectory() && fs.existsSync(path.join(skillDir, 'SKILL.md'));
|
|
43
|
+
})
|
|
44
|
+
.sort()
|
|
45
|
+
.map((name) => ({
|
|
46
|
+
name,
|
|
47
|
+
sourceDir: path.join(sourceRoot, name)
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveTargetDirectory(targetClient, scope, cwd, customPath) {
|
|
52
|
+
const client = TARGET_CLIENTS[targetClient];
|
|
53
|
+
if (!client) {
|
|
54
|
+
throw new Error(`Unsupported target client: ${targetClient}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (scope === 'custom') {
|
|
58
|
+
if (!customPath) {
|
|
59
|
+
throw new Error(`Missing custom path for ${targetClient}`);
|
|
60
|
+
}
|
|
61
|
+
return path.resolve(customPath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (scope === 'project') {
|
|
65
|
+
return client.projectDir(cwd);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return client.globalDir();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureDirectory(dirPath) {
|
|
72
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function copySkillDirectory(sourceDir, destDir, overwrite = false) {
|
|
76
|
+
if (fs.existsSync(destDir)) {
|
|
77
|
+
if (!overwrite) {
|
|
78
|
+
return 'exists';
|
|
79
|
+
}
|
|
80
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ensureDirectory(path.dirname(destDir));
|
|
84
|
+
fs.cpSync(sourceDir, destDir, { recursive: true });
|
|
85
|
+
return 'installed';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function promptForOverwrite(promptImpl, targetLabel, destDir) {
|
|
89
|
+
const response = await promptImpl({
|
|
90
|
+
type: 'select',
|
|
91
|
+
name: 'action',
|
|
92
|
+
message: `${targetLabel} already has ${path.basename(destDir)}. What should happen?`,
|
|
93
|
+
choices: [
|
|
94
|
+
{ title: 'Overwrite existing copy', value: 'overwrite' },
|
|
95
|
+
{ title: 'Skip this target', value: 'skip' },
|
|
96
|
+
{ title: 'Cancel installation', value: 'cancel' }
|
|
97
|
+
],
|
|
98
|
+
initial: 1
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return response.action;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function installSkillsWithPlan(plan, options = {}) {
|
|
105
|
+
const promptImpl = options.promptImpl || prompts;
|
|
106
|
+
const results = [];
|
|
107
|
+
|
|
108
|
+
for (const item of plan) {
|
|
109
|
+
const skillName = item.skill.name;
|
|
110
|
+
const destDir = path.join(item.destRoot, skillName);
|
|
111
|
+
let overwrite = false;
|
|
112
|
+
|
|
113
|
+
if (fs.existsSync(destDir)) {
|
|
114
|
+
const action = await promptForOverwrite(promptImpl, item.targetLabel, destDir);
|
|
115
|
+
if (!action || action === 'cancel') {
|
|
116
|
+
throw new Error('Installation cancelled.');
|
|
117
|
+
}
|
|
118
|
+
if (action === 'skip') {
|
|
119
|
+
results.push({ ...item, destDir, status: 'skipped' });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
overwrite = true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
copySkillDirectory(item.skill.sourceDir, destDir, overwrite);
|
|
126
|
+
results.push({ ...item, destDir, status: overwrite ? 'overwritten' : 'installed' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runInteractiveSkillInstaller(options = {}) {
|
|
133
|
+
const promptImpl = options.promptImpl || prompts;
|
|
134
|
+
const packageRoot = options.packageRoot || __dirname;
|
|
135
|
+
const cwd = options.cwd || process.cwd();
|
|
136
|
+
const packagedSkills = listPackagedSkills(packageRoot);
|
|
137
|
+
|
|
138
|
+
if (packagedSkills.length === 0) {
|
|
139
|
+
console.log('No packaged skills were found.');
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let selectedSkills = packagedSkills;
|
|
144
|
+
if (packagedSkills.length > 1) {
|
|
145
|
+
const selectedSkillsAnswer = await promptImpl({
|
|
146
|
+
type: 'multiselect',
|
|
147
|
+
name: 'skills',
|
|
148
|
+
message: 'Select skills to install',
|
|
149
|
+
choices: packagedSkills.map((skill) => ({
|
|
150
|
+
title: skill.name,
|
|
151
|
+
value: skill.name
|
|
152
|
+
})),
|
|
153
|
+
min: 1
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!selectedSkillsAnswer.skills || selectedSkillsAnswer.skills.length === 0) {
|
|
157
|
+
console.log('Skipped skill installation.');
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
selectedSkills = packagedSkills.filter((skill) => selectedSkillsAnswer.skills.includes(skill.name));
|
|
162
|
+
} else {
|
|
163
|
+
console.log(`Installing packaged skill: ${packagedSkills[0].name}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const selectedClientsAnswer = await promptImpl({
|
|
167
|
+
type: 'multiselect',
|
|
168
|
+
name: 'clients',
|
|
169
|
+
message: 'Select target clients',
|
|
170
|
+
choices: Object.entries(TARGET_CLIENTS).map(([key, value]) => ({
|
|
171
|
+
title: value.label,
|
|
172
|
+
value: key
|
|
173
|
+
})),
|
|
174
|
+
min: 1
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (!selectedClientsAnswer.clients || selectedClientsAnswer.clients.length === 0) {
|
|
178
|
+
console.log('Skipped skill installation.');
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const destinationSelections = {};
|
|
183
|
+
for (const clientKey of selectedClientsAnswer.clients) {
|
|
184
|
+
const client = TARGET_CLIENTS[clientKey];
|
|
185
|
+
const scopeAnswer = await promptImpl([
|
|
186
|
+
{
|
|
187
|
+
type: 'select',
|
|
188
|
+
name: 'scope',
|
|
189
|
+
message: `Install ${client.label} skills where?`,
|
|
190
|
+
choices: [
|
|
191
|
+
{
|
|
192
|
+
title: `Global (${client.globalDir()})`,
|
|
193
|
+
value: 'global'
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
title: `Current Project (${client.projectDir(cwd)})`,
|
|
197
|
+
value: 'project'
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
title: 'Custom Path',
|
|
201
|
+
value: 'custom'
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
initial: client.defaultScope === 'project' ? 1 : 0
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
type: (prev) => prev === 'custom' ? 'text' : null,
|
|
208
|
+
name: 'customPath',
|
|
209
|
+
message: `Custom ${client.label} skills path:`,
|
|
210
|
+
initial: client.projectDir(cwd),
|
|
211
|
+
validate: (value) => value ? true : 'Required'
|
|
212
|
+
}
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
if (!scopeAnswer.scope) {
|
|
216
|
+
console.log('Skipped skill installation.');
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
destinationSelections[clientKey] = {
|
|
221
|
+
scope: scopeAnswer.scope,
|
|
222
|
+
customPath: scopeAnswer.customPath || null
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const plan = [];
|
|
227
|
+
|
|
228
|
+
for (const clientKey of selectedClientsAnswer.clients) {
|
|
229
|
+
const client = TARGET_CLIENTS[clientKey];
|
|
230
|
+
const selection = destinationSelections[clientKey];
|
|
231
|
+
const destRoot = resolveTargetDirectory(clientKey, selection.scope, cwd, selection.customPath);
|
|
232
|
+
|
|
233
|
+
for (const skill of selectedSkills) {
|
|
234
|
+
plan.push({
|
|
235
|
+
targetClient: clientKey,
|
|
236
|
+
targetLabel: client.label,
|
|
237
|
+
scope: selection.scope,
|
|
238
|
+
destRoot,
|
|
239
|
+
skill
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log('\nInstalling telepty skills:');
|
|
245
|
+
for (const item of plan) {
|
|
246
|
+
console.log(` - ${item.skill.name} -> ${item.targetLabel} (${item.scope})`);
|
|
247
|
+
console.log(` ${item.destRoot}`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const results = await installSkillsWithPlan(plan, { promptImpl });
|
|
251
|
+
|
|
252
|
+
console.log('\nSkill installation results:');
|
|
253
|
+
for (const item of results) {
|
|
254
|
+
const label = item.status === 'skipped' ? 'Skipped' : 'Installed';
|
|
255
|
+
console.log(` ${label}: ${item.skill.name} -> ${item.targetLabel}`);
|
|
256
|
+
console.log(` ${item.destDir}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return results;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = {
|
|
263
|
+
TARGET_CLIENTS,
|
|
264
|
+
copySkillDirectory,
|
|
265
|
+
installSkillsWithPlan,
|
|
266
|
+
listPackagedSkills,
|
|
267
|
+
resolveTargetDirectory,
|
|
268
|
+
runInteractiveSkillInstaller
|
|
269
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# telepty
|
|
2
|
+
|
|
3
|
+
Use `telepty` to inspect active sessions, check the current telepty session ID, attach to sessions, inject commands, listen to the event bus, rename sessions, and update the daemon.
|
|
4
|
+
|
|
5
|
+
## When To Use
|
|
6
|
+
|
|
7
|
+
Use this skill when the user asks to:
|
|
8
|
+
- Check whether the current shell is running inside a telepty session
|
|
9
|
+
- List or inspect telepty sessions
|
|
10
|
+
- Attach to a telepty session
|
|
11
|
+
- Inject a prompt or command into another telepty session
|
|
12
|
+
- Listen to telepty bus events or publish a JSON payload
|
|
13
|
+
- Rename a session
|
|
14
|
+
- Update telepty
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
1. Check the current telepty session:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
echo "$TELEPTY_SESSION_ID"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
2. List sessions:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
telepty list
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
3. Attach to a session:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
telepty attach <session_id>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. Inject a prompt or command:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
telepty inject <session_id> "<prompt text>"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
5. Inject into multiple sessions:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
telepty multicast <id1,id2,...> "<prompt text>"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
6. Broadcast to all sessions:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
telepty broadcast "<prompt text>"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
7. Rename a session:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
telepty rename <old_id> <new_id>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
8. Listen to the event bus:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
telepty listen
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
9. Publish a JSON payload to the bus:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
TOKEN=$(grep authToken ~/.telepty/config.json | cut -d '"' -f 4)
|
|
70
|
+
curl -s -X POST http://127.0.0.1:3848/api/bus/publish \
|
|
71
|
+
-H "Content-Type: application/json" \
|
|
72
|
+
-H "x-telepty-token: $TOKEN" \
|
|
73
|
+
-d '{"type":"bg_message","payload":"..."}'
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
10. Update telepty:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
telepty update
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- `TELEPTY_SESSION_ID` is only set inside telepty-managed sessions.
|
|
85
|
+
- Use `telepty inject` when the target session should receive the command immediately.
|
|
86
|
+
- Use the JSON bus when the payload should be delivered without interrupting the target shell.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"topic": "Brainstorming and critiquing the 3 communication modes in telepty (Prompt Injection, Visual Log, Background JSON). What crucial interaction pattern between human users and AI agents is missing from this list? Consider synchronous vs asynchronous, blocking vs non-blocking, and explicit handshakes.", "speakers": ["claude", "codex", "gemini"], "role_preset": "brainstorm"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"topic": "Re-evaluate the telepty architecture based on the user`s strict boundaries: 1. A session equals a physical, visible terminal window. If the window closes, the session MUST die. No zombies. 2. Background persistence is NOT telepty`s job (users can use tmux if they want, but telepty doesn`t manage it). 3. Unexpected disconnects (network drop, sleep) are treated as session death, and recovery is handled via a higher-level `Handoff` of context to a new session, NOT by keeping zombie processes alive. Double-check this philosophy for flaws, focusing on how AI agents will communicate (Pub/Sub) within this strict ephemeral foreground model.", "speakers": ["claude", "codex", "gemini"], "role_preset": "review"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"topic": "Final architectural verification of the Telepty Boundary Model. The user asserts a hard line on responsibility: 1. If a session/process dies, context is lost. That is the CLI/Terminal`s problem, not telepty`s. 2. If a Pub message is sent to a dead Sub, it is lost. Telepty just drops it or relies on Ack. If the sub reconnects in time, it gets it; if not, drop it. 3. If Wi-Fi drops, CLI dies, agent dies, scripts die. Telepty does not care about split-brain or script recovery. Is this stateless, hyper-minimalist `dumb pipe` philosophy technically viable for our goals?", "speakers": ["claude", "gemini", "codex"], "role_preset": "consensus"}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# telepty
|
|
2
|
-
|
|
3
|
-
**Description:**
|
|
4
|
-
Help the user interact with the `telepty` daemon, check their current session ID, list active sessions, and inject commands or JSON events into remote or local PTY sessions. All operations are performed using standard CLI commands or `curl`.
|
|
5
|
-
|
|
6
|
-
**Trigger:**
|
|
7
|
-
When the user asks about their current session ID, wants to check active sessions, wants to inject a prompt/command into a specific session, wants to send a JSON event via the bus, wants to subscribe/listen to the bus, or wants to update telepty.
|
|
8
|
-
|
|
9
|
-
**Instructions:**
|
|
10
|
-
1. **To check the current session ID:**
|
|
11
|
-
- Execute `run_shell_command` with `echo $TELEPTY_SESSION_ID`.
|
|
12
|
-
- If the value is empty, inform the user that the current shell is *not* running inside a telepty spawned session.
|
|
13
|
-
- If it has a value, output it clearly.
|
|
14
|
-
2. **To list all sessions:**
|
|
15
|
-
- Run `telepty list`.
|
|
16
|
-
3. **To send a message/command to another agent, you must choose ONE of three methods depending on the user's intent:**
|
|
17
|
-
|
|
18
|
-
**Method A: Prompt Injection (Active Interruption)**
|
|
19
|
-
- Use this when you want the receiving AI to IMMEDIATELY read and execute the message as a prompt.
|
|
20
|
-
- Run: `telepty inject <target_session_id> "<prompt text>"`
|
|
21
|
-
- (For multiple: `telepty multicast <id1>,<id2> "<prompt>"`)
|
|
22
|
-
|
|
23
|
-
**Method B: Log Injection (Visual Notification)**
|
|
24
|
-
- Use this when you want the message to appear immediately on the receiving terminal's screen for the user to see, but WITHOUT forcing the AI to execute it as a prompt.
|
|
25
|
-
- Run: `telepty inject <target_session_id> "echo '\x1b[33m[📬 Message from $TELEPTY_SESSION_ID]\x1b[0m <message text>'"`
|
|
26
|
-
|
|
27
|
-
**Method C: Background JSON Bus (Passive/Silent)**
|
|
28
|
-
- Use this for structured data transfer that the other AI will read later from its log file, without disturbing its current terminal screen.
|
|
29
|
-
- Run:
|
|
30
|
-
```bash
|
|
31
|
-
TOKEN=$(cat ~/.telepty/config.json | grep authToken | cut -d '"' -f 4)
|
|
32
|
-
curl -s -X POST http://127.0.0.1:3848/api/bus/publish -H "Content-Type: application/json" -H "x-telepty-token: $TOKEN" -d '{"type": "bg_message", "payload": "..."}'
|
|
33
|
-
```
|
|
34
|
-
4. **To subscribe to the Event Bus (Listen for JSON events):**
|
|
35
|
-
- Run `nohup telepty listen > .telepty_bus_events.log 2>&1 &`
|
|
36
|
-
5. **To physically OPEN a new Terminal Window for the user (macOS):**
|
|
37
|
-
- If the user asks you to "open a new telepty terminal" or "방 파줘", you can physically spawn a new Ghostty/Terminal window on their screen that is already attached to a telepty session.
|
|
38
|
-
- Run this shell command (replace `<ID>` and `<CMD>`):
|
|
39
|
-
```bash
|
|
40
|
-
cat << 'EOF' > /tmp/telepty-auto.command
|
|
41
|
-
#!/bin/bash
|
|
42
|
-
telepty spawn --id <ID> <CMD>
|
|
43
|
-
EOF
|
|
44
|
-
chmod +x /tmp/telepty-auto.command
|
|
45
|
-
open -a Ghostty /tmp/telepty-auto.command || open /tmp/telepty-auto.command
|
|
46
|
-
```
|
|
47
|
-
6. **To rename a session:**
|
|
48
|
-
- Run `telepty rename <old_id> <new_id>`
|
|
49
|
-
- This updates the session key, Ghostty tab title, and broadcasts a `session_rename` event on the bus.
|
|
50
|
-
7. **Terminal Title Convention:**
|
|
51
|
-
- Each telepty session displays its ID in the Ghostty tab title.
|
|
52
|
-
- Local: `⚡ telepty :: {session_id}`
|
|
53
|
-
- Remote: `⚡ telepty :: {session_id} @ {host}`
|
|
54
|
-
8. **To update telepty:**
|
|
55
|
-
- Run `telepty update`.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
name: Regression Tests
|
|
2
|
-
on:
|
|
3
|
-
push:
|
|
4
|
-
branches: [ "main" ]
|
|
5
|
-
pull_request:
|
|
6
|
-
branches: [ "main" ]
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
strategy:
|
|
12
|
-
fail-fast: false
|
|
13
|
-
matrix:
|
|
14
|
-
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
15
|
-
node: [20]
|
|
16
|
-
runs-on: ${{ matrix.os }}
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v4
|
|
19
|
-
- uses: actions/setup-node@v4
|
|
20
|
-
with:
|
|
21
|
-
node-version: ${{ matrix.node }}
|
|
22
|
-
cache: npm
|
|
23
|
-
- name: Install dependencies
|
|
24
|
-
run: npm ci
|
|
25
|
-
- name: Run regression suite
|
|
26
|
-
run: npm run test:ci
|
|
Binary file
|
package/clipboard_image.png
DELETED
|
Binary file
|
package/test/auth.test.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { test, before, after } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
|
|
9
|
-
// Use a temp directory to isolate tests from the real ~/.telepty config.
|
|
10
|
-
// We patch os.homedir() before requiring auth.js so the module-level
|
|
11
|
-
// constants (CONFIG_DIR, CONFIG_FILE) resolve to the temp dir.
|
|
12
|
-
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'telepty-test-'));
|
|
13
|
-
const originalHomedir = os.homedir.bind(os);
|
|
14
|
-
|
|
15
|
-
before(() => {
|
|
16
|
-
os.homedir = () => tmpDir;
|
|
17
|
-
// Ensure auth.js is loaded fresh with the patched homedir
|
|
18
|
-
delete require.cache[require.resolve('../auth.js')];
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
after(() => {
|
|
22
|
-
os.homedir = originalHomedir;
|
|
23
|
-
delete require.cache[require.resolve('../auth.js')];
|
|
24
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('getConfig() returns an object with authToken property', () => {
|
|
28
|
-
const { getConfig } = require('../auth.js');
|
|
29
|
-
const config = getConfig();
|
|
30
|
-
assert.ok(config !== null && typeof config === 'object', 'config should be an object');
|
|
31
|
-
assert.ok('authToken' in config, 'config should have authToken property');
|
|
32
|
-
assert.equal(typeof config.authToken, 'string', 'authToken should be a string');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('authToken is a valid UUID v4 format', () => {
|
|
36
|
-
const { getConfig } = require('../auth.js');
|
|
37
|
-
const config = getConfig();
|
|
38
|
-
const uuidV4Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
39
|
-
assert.match(config.authToken, uuidV4Regex, 'authToken should be a valid UUID v4');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('config object has createdAt field', () => {
|
|
43
|
-
const { getConfig } = require('../auth.js');
|
|
44
|
-
const config = getConfig();
|
|
45
|
-
assert.ok('createdAt' in config, 'config should have createdAt property');
|
|
46
|
-
const date = new Date(config.createdAt);
|
|
47
|
-
assert.ok(!isNaN(date.getTime()), 'createdAt should be a valid ISO date string');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('calling getConfig() twice returns the same token (persistence)', () => {
|
|
51
|
-
const { getConfig } = require('../auth.js');
|
|
52
|
-
const config1 = getConfig();
|
|
53
|
-
const config2 = getConfig();
|
|
54
|
-
assert.equal(config1.authToken, config2.authToken, 'authToken should be identical across calls');
|
|
55
|
-
assert.equal(config1.createdAt, config2.createdAt, 'createdAt should be identical across calls');
|
|
56
|
-
});
|
package/test/cli.test.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { afterEach, beforeEach, test } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const { createSessionId, startTestDaemon, stripAnsi, waitFor } = require('../test-support/daemon-harness');
|
|
6
|
-
|
|
7
|
-
let harness;
|
|
8
|
-
|
|
9
|
-
function collectJsonMessages(ws) {
|
|
10
|
-
const messages = [];
|
|
11
|
-
ws.on('message', (chunk) => {
|
|
12
|
-
try {
|
|
13
|
-
messages.push(JSON.parse(chunk.toString()));
|
|
14
|
-
} catch {
|
|
15
|
-
// Ignore malformed payloads in tests.
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
return messages;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
beforeEach(async () => {
|
|
22
|
-
harness = await startTestDaemon();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
afterEach(async () => {
|
|
26
|
-
await harness.stop();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('telepty list prints active sessions from the configured host and port', async () => {
|
|
30
|
-
const sessionId = createSessionId('cli-list');
|
|
31
|
-
await harness.spawnSession(sessionId);
|
|
32
|
-
|
|
33
|
-
const result = await harness.runCli(['list']);
|
|
34
|
-
assert.equal(result.code, 0, result.stderr);
|
|
35
|
-
|
|
36
|
-
const output = stripAnsi(`${result.stdout}\n${result.stderr}`);
|
|
37
|
-
assert.match(output, new RegExp(sessionId));
|
|
38
|
-
assert.match(output, /Active Sessions/i);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('telepty inject forwards input to the target PTY session', async () => {
|
|
42
|
-
const sessionId = createSessionId('cli-inject');
|
|
43
|
-
await harness.spawnSession(sessionId);
|
|
44
|
-
|
|
45
|
-
const ws = await harness.connectSession(sessionId);
|
|
46
|
-
const outputs = collectJsonMessages(ws);
|
|
47
|
-
const token = createSessionId('cli-token');
|
|
48
|
-
|
|
49
|
-
const result = await harness.runCli(['inject', sessionId, `echo ${token}`]);
|
|
50
|
-
assert.equal(result.code, 0, result.stderr);
|
|
51
|
-
|
|
52
|
-
await waitFor(() => outputs.some((message) => (
|
|
53
|
-
message.type === 'output' && String(message.data).includes(token)
|
|
54
|
-
)), { timeoutMs: 7000, description: 'CLI inject output' });
|
|
55
|
-
|
|
56
|
-
ws.close();
|
|
57
|
-
});
|