wtt-connect 0.2.50 → 0.2.52
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/package.json +1 -1
- package/src/main.js +13 -2
- package/src/runner.js +3 -2
- package/src/service-manager.js +157 -8
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -20,6 +20,7 @@ export async function main(args) {
|
|
|
20
20
|
const argv = parseArgs(args.slice(1));
|
|
21
21
|
if (!argv.envFile && argv.profile) argv.envFile = resolveProfileEnvFile(argv.profile);
|
|
22
22
|
loadDefaultEnvFiles({ envFile: argv.envFile });
|
|
23
|
+
if (cmd === 'version' || cmd === '--version' || cmd === '-v') return printVersion();
|
|
23
24
|
if (cmd === 'help' || cmd === '--help' || cmd === '-h') return printHelp();
|
|
24
25
|
if (cmd === 'up' || cmd === 'link') return up(argv);
|
|
25
26
|
if (cmd === 'list') return listProfiles(argv);
|
|
@@ -109,6 +110,15 @@ function parseArgs(args) {
|
|
|
109
110
|
return out;
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function printVersion() {
|
|
114
|
+
try {
|
|
115
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
116
|
+
console.log(pkg.version || 'unknown');
|
|
117
|
+
} catch {
|
|
118
|
+
console.log('unknown');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
function printHelp() {
|
|
113
123
|
console.log(`wtt-connect
|
|
114
124
|
|
|
@@ -138,7 +148,7 @@ Commands:
|
|
|
138
148
|
opendesign-upload --dir <path>
|
|
139
149
|
Alias for upload-artifact
|
|
140
150
|
preview-port --port <port> [--topic-id <id>] [--snapshot-dir <dir>]
|
|
141
|
-
Create a Cloud Sandbox preview URL
|
|
151
|
+
Create a Cloud Sandbox live preview URL; snapshots are opt-in
|
|
142
152
|
cleanup-previews Stop preview servers previously registered by this agent
|
|
143
153
|
help Show this help
|
|
144
154
|
`);
|
|
@@ -163,7 +173,8 @@ async function previewPort(config, argv) {
|
|
|
163
173
|
const url = preview.preview_url || preview.url;
|
|
164
174
|
if (!url) throw new Error('Cloud Sandbox preview API returned no URL');
|
|
165
175
|
const title = String(argv.title || argv.previewName || preview.name || 'Cloud Sandbox Preview').trim();
|
|
166
|
-
const
|
|
176
|
+
const wantsSnapshot = Boolean(String(argv.snapshotDir || '').trim()) && !argv.noSnapshot;
|
|
177
|
+
const snapshot = wantsSnapshot ? await createPreviewSnapshot(config, argv, { title }) : null;
|
|
167
178
|
const snapshotUrl = snapshot?.snapshot_url || '';
|
|
168
179
|
const markdown = JSON.stringify({
|
|
169
180
|
type: 'cloud_sandbox_preview',
|
package/src/runner.js
CHANGED
|
@@ -943,11 +943,12 @@ function renderCloudSandboxStorageInstruction(config, topicId = '') {
|
|
|
943
943
|
'- Before publishing a preview URL, verify the server is actually listening and serving content with `curl -fsS http://127.0.0.1:<port>/ >/dev/null`.',
|
|
944
944
|
'- If local curl fails, fix or restart the web server first. Never publish a preview URL for a dead port.',
|
|
945
945
|
'- `wtt-connect preview-port` automatically keeps the most recent preview servers for this agent and stops older registered previews beyond the retention limit. Default retention is 3 live previews; use `--keep-last <n>` only when the user asks.',
|
|
946
|
-
'-
|
|
946
|
+
'- Do not pass `--snapshot-dir` for normal live previews. Live Cloud Sandbox previews should render as inline WTT preview cards, not as ordinary index.html/document artifacts.',
|
|
947
|
+
'- If the user explicitly asks for a persistent static artifact or downloadable HTML, publish that separately with `wtt-connect upload-artifact --dir <dir> --title "Short Title"` instead of mixing it with the live preview URL.',
|
|
947
948
|
'- If you start a web server in the sandbox and the user should preview it, call the Cloudflare Sandbox outbound Worker directly with curl and include the returned `preview_url` in your reply as `[preview_url:Short Title](<preview_url>)`.',
|
|
948
949
|
'- Preview ports must be 1024-65535 and cannot be 3000. For Vite/Next-style dev servers prefer 5173, 4173, or 8080.',
|
|
949
950
|
`- Preview curl rule: curl -sS -X POST "\${WTT_SANDBOX_OUTBOX_URL:-http://wtt.preview}/preview-port" -H 'content-type: application/json' -d '{"agent_id":"'\${WTT_AGENT_ID:-cloud-agent}'","port":<port>}'`,
|
|
950
|
-
`- Prefer automatic WTT publishing when topic id is available: wtt-connect preview-port --port <port>${topicId ? ` --topic-id ${topicId}` : ' --topic-id <topic_id>'} --title "Short Title"
|
|
951
|
+
`- Prefer automatic WTT publishing when topic id is available: wtt-connect preview-port --port <port>${topicId ? ` --topic-id ${topicId}` : ' --topic-id <topic_id>'} --title "Short Title". This publishes one cloud_sandbox_preview live card.`,
|
|
951
952
|
);
|
|
952
953
|
return lines.join('\n');
|
|
953
954
|
}
|
package/src/service-manager.js
CHANGED
|
@@ -87,12 +87,16 @@ export async function up(argv) {
|
|
|
87
87
|
|
|
88
88
|
export function listProfiles() {
|
|
89
89
|
const dir = profilesDir();
|
|
90
|
+
const sandboxEntries = discoverCloudSandboxEntries();
|
|
90
91
|
if (!fs.existsSync(dir)) {
|
|
91
|
-
console.log('no profiles');
|
|
92
|
+
if (!sandboxEntries.length) console.log('no profiles');
|
|
93
|
+
for (const entry of sandboxEntries) {
|
|
94
|
+
console.log(`${entry.profile}\t${entry.adapter || 'cloud-sandbox'}\t${entry.agentId}\tcloud-sandbox`);
|
|
95
|
+
}
|
|
92
96
|
return;
|
|
93
97
|
}
|
|
94
98
|
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.env')).sort();
|
|
95
|
-
if (!files.length) {
|
|
99
|
+
if (!files.length && !sandboxEntries.length) {
|
|
96
100
|
console.log('no profiles');
|
|
97
101
|
return;
|
|
98
102
|
}
|
|
@@ -101,23 +105,36 @@ export function listProfiles() {
|
|
|
101
105
|
const env = readEnv(path.join(dir, file));
|
|
102
106
|
console.log(`${profile}\t${env.WTT_CONNECT_ADAPTER || ''}\t${env.WTT_AGENT_ID || ''}`);
|
|
103
107
|
}
|
|
108
|
+
for (const entry of sandboxEntries) {
|
|
109
|
+
console.log(`${entry.profile}\t${entry.adapter || 'cloud-sandbox'}\t${entry.agentId}\tcloud-sandbox`);
|
|
110
|
+
}
|
|
104
111
|
}
|
|
105
112
|
|
|
106
113
|
export function status(argv) {
|
|
107
|
-
const profiles = targetProfiles(argv);
|
|
108
|
-
|
|
114
|
+
const profiles = targetProfiles(argv, { allowEmpty: true });
|
|
115
|
+
const sandboxEntries = targetCloudSandboxEntries(argv);
|
|
116
|
+
if (!profiles.length && !sandboxEntries.length) {
|
|
109
117
|
console.log('no profiles');
|
|
110
118
|
return;
|
|
111
119
|
}
|
|
112
120
|
for (const profile of profiles) {
|
|
113
121
|
console.log(`${profile}: ${serviceStatus(profile)}`);
|
|
114
122
|
}
|
|
123
|
+
for (const entry of sandboxEntries) {
|
|
124
|
+
console.log(`${entry.profile}: ${cloudSandboxStatus(entry)}`);
|
|
125
|
+
}
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
export function restart(argv) {
|
|
118
|
-
const profiles = targetProfiles(argv);
|
|
129
|
+
const profiles = targetProfiles(argv, { allowEmpty: true });
|
|
130
|
+
const sandboxEntries = targetCloudSandboxEntries(argv);
|
|
131
|
+
if (!profiles.length && !sandboxEntries.length) {
|
|
132
|
+
console.log('no profiles');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
119
135
|
for (const profile of profiles) restartService(profile);
|
|
120
|
-
|
|
136
|
+
for (const entry of sandboxEntries) restartCloudSandboxEntry(entry);
|
|
137
|
+
status({ _: [...profiles, ...sandboxEntries.map((entry) => entry.profile)] });
|
|
121
138
|
}
|
|
122
139
|
|
|
123
140
|
export function logs(argv) {
|
|
@@ -182,11 +199,17 @@ export function down(argv) {
|
|
|
182
199
|
console.log(`kept state dir: ${defaultStateDir(safe)}`);
|
|
183
200
|
}
|
|
184
201
|
|
|
185
|
-
function targetProfiles(argv) {
|
|
202
|
+
function targetProfiles(argv, options = {}) {
|
|
186
203
|
const requested = argv._ || [];
|
|
187
|
-
if (requested.length && requested[0] !== 'all')
|
|
204
|
+
if (requested.length && requested[0] !== 'all') {
|
|
205
|
+
const available = new Set(discoverProfiles());
|
|
206
|
+
const profiles = requested.map(sanitizeProfile).filter((profile) => available.has(profile));
|
|
207
|
+
if (profiles.length || options.allowEmpty) return profiles;
|
|
208
|
+
return requested.map(sanitizeProfile);
|
|
209
|
+
}
|
|
188
210
|
const profiles = discoverProfiles();
|
|
189
211
|
if (!profiles.length && requested[0] === 'all') return [];
|
|
212
|
+
if (!profiles.length && options.allowEmpty) return [];
|
|
190
213
|
if (!profiles.length) throw new Error('no wtt-connect profiles found');
|
|
191
214
|
return profiles;
|
|
192
215
|
}
|
|
@@ -197,6 +220,132 @@ function discoverProfiles() {
|
|
|
197
220
|
return fs.readdirSync(dir).filter((f) => f.endsWith('.env')).map((f) => sanitizeProfile(f.slice(0, -4))).sort();
|
|
198
221
|
}
|
|
199
222
|
|
|
223
|
+
function targetCloudSandboxEntries(argv) {
|
|
224
|
+
const requested = (argv._ || []).map((value) => String(value || '').trim()).filter(Boolean);
|
|
225
|
+
const entries = discoverCloudSandboxEntries();
|
|
226
|
+
if (!requested.length || requested[0] === 'all') return entries;
|
|
227
|
+
const wanted = new Set(requested.map(sanitizeProfile));
|
|
228
|
+
return entries.filter((entry) => wanted.has(entry.profile) || wanted.has(entry.agentId));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function discoverCloudSandboxEntries() {
|
|
232
|
+
const root = path.join(os.homedir(), '.wtt-connect', 'agent');
|
|
233
|
+
if (!fs.existsSync(root)) return [];
|
|
234
|
+
const entries = [];
|
|
235
|
+
for (const name of fs.readdirSync(root).sort()) {
|
|
236
|
+
const agentRoot = path.join(root, name);
|
|
237
|
+
const envFile = path.join(agentRoot, 'run', 'wtt-connect.env');
|
|
238
|
+
if (!fs.existsSync(envFile)) continue;
|
|
239
|
+
const env = readEnv(envFile);
|
|
240
|
+
const agentId = sanitizeProfile(env.WTT_AGENT_ID || name);
|
|
241
|
+
if (!agentId) continue;
|
|
242
|
+
entries.push({
|
|
243
|
+
profile: agentId,
|
|
244
|
+
agentId,
|
|
245
|
+
adapter: env.WTT_CONNECT_ADAPTER || env.WTT_CONNECT_ADAPTERS || '',
|
|
246
|
+
envFile,
|
|
247
|
+
agentRoot,
|
|
248
|
+
pidFile: path.join(agentRoot, 'run', 'wtt-connect.pid'),
|
|
249
|
+
logFile: path.join(agentRoot, 'logs', 'wtt-connect.log'),
|
|
250
|
+
workDir: env.WTT_CONNECT_WORKDIR || path.join(agentRoot, 'workspace'),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return entries;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function cloudSandboxStatus(entry) {
|
|
257
|
+
const pid = readPid(entry.pidFile);
|
|
258
|
+
if (pid && processRunningWithAgent(pid, entry.agentId)) return `active pid=${pid}`;
|
|
259
|
+
const found = findPidByAgentEnv(entry.agentId);
|
|
260
|
+
if (found) return `active pid=${found}`;
|
|
261
|
+
return 'inactive';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function restartCloudSandboxEntry(entry) {
|
|
265
|
+
stopCloudSandboxEntry(entry);
|
|
266
|
+
ensureDir(path.dirname(entry.pidFile));
|
|
267
|
+
ensureDir(path.dirname(entry.logFile));
|
|
268
|
+
ensureDir(entry.workDir);
|
|
269
|
+
const bin = path.join(packageRoot(), 'bin', 'wtt-connect.js');
|
|
270
|
+
const script = [
|
|
271
|
+
`set -a`,
|
|
272
|
+
`. ${shellQuote(entry.envFile)}`,
|
|
273
|
+
`set +a`,
|
|
274
|
+
`export PATH=${shellQuote(runtimePath())}`,
|
|
275
|
+
`mkdir -p ${shellQuote(entry.workDir)} ${shellQuote(path.dirname(entry.pidFile))} ${shellQuote(path.dirname(entry.logFile))}`,
|
|
276
|
+
`cd ${shellQuote(entry.workDir)}`,
|
|
277
|
+
`nohup ${shellQuote(process.execPath)} ${shellQuote(bin)} start --env-file ${shellQuote(entry.envFile)} >> ${shellQuote(entry.logFile)} 2>&1 < /dev/null &`,
|
|
278
|
+
`echo $! > ${shellQuote(entry.pidFile)}`,
|
|
279
|
+
].join('; ');
|
|
280
|
+
const result = spawnSync('bash', ['-lc', script], { encoding: 'utf8' });
|
|
281
|
+
if (result.status !== 0) {
|
|
282
|
+
throw new Error(`cloud sandbox restart failed for ${entry.agentId}: ${result.stderr || result.stdout}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function stopCloudSandboxEntry(entry) {
|
|
287
|
+
const pids = new Set();
|
|
288
|
+
const pid = readPid(entry.pidFile);
|
|
289
|
+
if (pid) pids.add(pid);
|
|
290
|
+
const envPid = findPidByAgentEnv(entry.agentId);
|
|
291
|
+
if (envPid) pids.add(envPid);
|
|
292
|
+
for (const target of pids) {
|
|
293
|
+
try { process.kill(target, 'SIGTERM'); } catch {}
|
|
294
|
+
}
|
|
295
|
+
const deadline = Date.now() + 2000;
|
|
296
|
+
while (Date.now() < deadline && Array.from(pids).some((target) => processAlive(target))) {
|
|
297
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
298
|
+
}
|
|
299
|
+
for (const target of pids) {
|
|
300
|
+
if (!processAlive(target)) continue;
|
|
301
|
+
try { process.kill(target, 'SIGKILL'); } catch {}
|
|
302
|
+
}
|
|
303
|
+
try { fs.rmSync(entry.pidFile); } catch {}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function readPid(file) {
|
|
307
|
+
try {
|
|
308
|
+
const pid = Number.parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
|
|
309
|
+
return Number.isFinite(pid) && pid > 0 ? pid : 0;
|
|
310
|
+
} catch {
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function processAlive(pid) {
|
|
316
|
+
try {
|
|
317
|
+
process.kill(pid, 0);
|
|
318
|
+
return true;
|
|
319
|
+
} catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function processRunningWithAgent(pid, agentId) {
|
|
325
|
+
if (!processAlive(pid)) return false;
|
|
326
|
+
try {
|
|
327
|
+
const env = fs.readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0');
|
|
328
|
+
return env.includes(`WTT_AGENT_ID=${agentId}`);
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function findPidByAgentEnv(agentId) {
|
|
335
|
+
if (process.platform !== 'linux') return 0;
|
|
336
|
+
for (const name of fs.readdirSync('/proc')) {
|
|
337
|
+
if (!/^\d+$/.test(name)) continue;
|
|
338
|
+
const pid = Number.parseInt(name, 10);
|
|
339
|
+
if (pid === process.pid) continue;
|
|
340
|
+
if (processRunningWithAgent(pid, agentId)) return pid;
|
|
341
|
+
}
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function shellQuote(value) {
|
|
346
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
347
|
+
}
|
|
348
|
+
|
|
200
349
|
function writeProfileEnv(file, values) {
|
|
201
350
|
const lines = [
|
|
202
351
|
'# wtt-connect profile',
|