@nocobase/cli 2.1.0-beta.27 → 2.1.0-beta.30

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.
Files changed (50) hide show
  1. package/README.md +14 -0
  2. package/README.zh-CN.md +14 -0
  3. package/bin/run.js +3 -0
  4. package/bin/session-env.js +27 -0
  5. package/dist/commands/app/down.js +47 -9
  6. package/dist/commands/app/logs.js +17 -0
  7. package/dist/commands/app/restart.js +23 -1
  8. package/dist/commands/app/start.js +17 -0
  9. package/dist/commands/app/stop.js +17 -0
  10. package/dist/commands/app/upgrade.js +22 -2
  11. package/dist/commands/db/check.js +6 -4
  12. package/dist/commands/db/ps.js +1 -1
  13. package/dist/commands/env/add.js +3 -2
  14. package/dist/commands/env/auth.js +1 -1
  15. package/dist/commands/env/current.js +21 -0
  16. package/dist/commands/env/info.js +4 -3
  17. package/dist/commands/env/list.js +8 -14
  18. package/dist/commands/env/remove.js +2 -2
  19. package/dist/commands/env/status.js +90 -0
  20. package/dist/commands/env/update.js +1 -1
  21. package/dist/commands/env/use.js +11 -1
  22. package/dist/commands/install.js +10 -4
  23. package/dist/commands/license/activate.js +20 -24
  24. package/dist/commands/license/id.js +17 -2
  25. package/dist/commands/license/plugins/clean.js +17 -2
  26. package/dist/commands/license/plugins/list.js +17 -2
  27. package/dist/commands/license/plugins/sync.js +22 -5
  28. package/dist/commands/license/shared.js +15 -6
  29. package/dist/commands/license/status.js +17 -2
  30. package/dist/commands/plugin/disable.js +25 -4
  31. package/dist/commands/plugin/enable.js +25 -4
  32. package/dist/commands/plugin/list.js +25 -4
  33. package/dist/commands/session/id.js +24 -0
  34. package/dist/commands/session/remove.js +57 -0
  35. package/dist/commands/session/setup.js +62 -0
  36. package/dist/commands/source/dev.js +19 -1
  37. package/dist/commands/source/download.js +10 -8
  38. package/dist/lib/app-managed-resources.js +5 -3
  39. package/dist/lib/app-runtime.js +1 -1
  40. package/dist/lib/auth-store.js +28 -11
  41. package/dist/lib/docker-image.js +37 -0
  42. package/dist/lib/env-guard.js +61 -0
  43. package/dist/lib/generated-command.js +16 -0
  44. package/dist/lib/plugin-storage.js +1 -64
  45. package/dist/lib/resource-command.js +15 -0
  46. package/dist/lib/runtime-generator.js +1 -1
  47. package/dist/lib/session-id.js +17 -0
  48. package/dist/lib/session-integration.js +703 -0
  49. package/dist/lib/session-store.js +118 -0
  50. package/package.json +3 -3
@@ -0,0 +1,703 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ /**
10
+ * This file is part of the NocoBase (R) project.
11
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
12
+ * Authors: NocoBase Team.
13
+ *
14
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
15
+ * For more information, please refer to: https://www.nocobase.com/agreement.
16
+ */
17
+ import { execFile, execFileSync } from 'node:child_process';
18
+ import { promises as fs } from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { promisify } from 'node:util';
22
+ import { resolveCliHomeDir } from './cli-home.js';
23
+ const START_MARKER = '# >>> nocobase nb session >>>';
24
+ const END_MARKER = '# <<< nocobase nb session <<<';
25
+ const OPENCODE_PLUGIN_NAME = 'nb-agent-session.js';
26
+ const CMD_AUTORUN_REGISTRY_KEY = 'HKCU\\Software\\Microsoft\\Command Processor';
27
+ const CMD_AUTORUN_REGISTRY_VALUE = 'AutoRun';
28
+ const CMD_AUTORUN_OVERRIDE_FILE_ENV = 'NB_SESSION_CMD_AUTORUN_FILE';
29
+ const WINDOWS_PARENT_PROCESS_OVERRIDE_ENV = 'NB_SESSION_TEST_PARENT_PROCESS_NAME';
30
+ const execFileAsync = promisify(execFile);
31
+ function shellDir() {
32
+ return path.join(resolveCliHomeDir(), 'shell');
33
+ }
34
+ function resolveSessionHomeDir() {
35
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
36
+ }
37
+ function opencodeConfigDir(shell) {
38
+ return path.join(resolveSessionHomeDir(), '.config', 'opencode');
39
+ }
40
+ function opencodePluginFilePath(shell) {
41
+ return path.join(opencodeConfigDir(shell), 'plugins', OPENCODE_PLUGIN_NAME);
42
+ }
43
+ function opencodeConfigFilePath(shell) {
44
+ return path.join(opencodeConfigDir(shell), 'opencode.json');
45
+ }
46
+ async function pathExists(filePath) {
47
+ try {
48
+ await fs.access(filePath);
49
+ return true;
50
+ }
51
+ catch (_error) {
52
+ return false;
53
+ }
54
+ }
55
+ function managedFilePath(shell) {
56
+ const dir = shellDir();
57
+ switch (shell) {
58
+ case 'bash':
59
+ return path.join(dir, 'session.bash');
60
+ case 'zsh':
61
+ return path.join(dir, 'session.zsh');
62
+ case 'fish':
63
+ return path.join(dir, 'session.fish');
64
+ case 'powershell':
65
+ return path.join(dir, 'session.ps1');
66
+ case 'cmd':
67
+ return path.join(dir, 'nb.cmd');
68
+ default:
69
+ return path.join(dir, 'session.sh');
70
+ }
71
+ }
72
+ function detectWindowsPowerShellProfiles() {
73
+ const home = resolveSessionHomeDir();
74
+ const modern = path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
75
+ const legacy = path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
76
+ return Array.from(new Set([modern, legacy]));
77
+ }
78
+ function normalizeShellHint(shellHint) {
79
+ const normalized = shellHint.trim().toLowerCase();
80
+ if (!normalized) {
81
+ return undefined;
82
+ }
83
+ if (normalized === 'fish' || normalized === 'fish.exe') {
84
+ return 'fish';
85
+ }
86
+ if (normalized === 'zsh' || normalized === 'zsh.exe') {
87
+ return 'zsh';
88
+ }
89
+ if (normalized === 'bash' || normalized === 'bash.exe') {
90
+ return 'bash';
91
+ }
92
+ if (normalized === 'powershell' || normalized === 'powershell.exe' || normalized === 'pwsh' || normalized === 'pwsh.exe') {
93
+ return 'powershell';
94
+ }
95
+ if (normalized === 'cmd' || normalized === 'cmd.exe') {
96
+ return 'cmd';
97
+ }
98
+ const normalizedPath = normalized.replace(/\\/g, '/');
99
+ if (!normalizedPath) {
100
+ return undefined;
101
+ }
102
+ if (normalizedPath.endsWith('/zsh') || normalizedPath.endsWith('/zsh.exe')) {
103
+ return 'zsh';
104
+ }
105
+ if (normalizedPath.endsWith('/bash') || normalizedPath.endsWith('/bash.exe')) {
106
+ return 'bash';
107
+ }
108
+ if (normalizedPath.endsWith('/fish') || normalizedPath.endsWith('/fish.exe')) {
109
+ return 'fish';
110
+ }
111
+ if (normalizedPath.endsWith('/pwsh') || normalizedPath.endsWith('/pwsh.exe')) {
112
+ return 'powershell';
113
+ }
114
+ if (normalizedPath.endsWith('/powershell') || normalizedPath.endsWith('/powershell.exe')) {
115
+ return 'powershell';
116
+ }
117
+ if (normalizedPath.endsWith('/cmd') || normalizedPath.endsWith('/cmd.exe')) {
118
+ return 'cmd';
119
+ }
120
+ return undefined;
121
+ }
122
+ function normalizeWindowsShellProcessName(processName) {
123
+ const normalized = processName.trim().toLowerCase();
124
+ if (normalized === 'cmd' || normalized === 'cmd.exe') {
125
+ return 'cmd';
126
+ }
127
+ if (normalized === 'fish' || normalized === 'fish.exe') {
128
+ return 'fish';
129
+ }
130
+ if (normalized === 'zsh' || normalized === 'zsh.exe') {
131
+ return 'zsh';
132
+ }
133
+ if (normalized === 'bash'
134
+ || normalized === 'bash.exe'
135
+ || normalized === 'git-bash'
136
+ || normalized === 'git-bash.exe') {
137
+ return 'bash';
138
+ }
139
+ if (normalized === 'powershell' || normalized === 'powershell.exe' || normalized === 'pwsh' || normalized === 'pwsh.exe') {
140
+ return 'powershell';
141
+ }
142
+ return undefined;
143
+ }
144
+ function resolveWindowsShellFromProcessChain(processNames) {
145
+ const normalizedShells = processNames
146
+ .map((processName) => normalizeWindowsShellProcessName(processName))
147
+ .filter((shell) => Boolean(shell));
148
+ if (normalizedShells.length === 0) {
149
+ return undefined;
150
+ }
151
+ let leadingCmdCount = 0;
152
+ while (normalizedShells[leadingCmdCount] === 'cmd') {
153
+ leadingCmdCount += 1;
154
+ }
155
+ if (leadingCmdCount > 0 && normalizedShells[leadingCmdCount]) {
156
+ return normalizedShells[leadingCmdCount];
157
+ }
158
+ return normalizedShells[0];
159
+ }
160
+ function detectWindowsShellByParentProcess() {
161
+ if (process.platform !== 'win32' || !process.ppid) {
162
+ return undefined;
163
+ }
164
+ const overrideParentProcess = String(process.env[WINDOWS_PARENT_PROCESS_OVERRIDE_ENV] ?? '').trim();
165
+ if (overrideParentProcess) {
166
+ return resolveWindowsShellFromProcessChain(overrideParentProcess.split(/[,\r\n]+/));
167
+ }
168
+ try {
169
+ return resolveWindowsShellFromProcessChain(String(execFileSync('powershell.exe', [
170
+ '-NoProfile',
171
+ '-Command',
172
+ [
173
+ `$id = ${process.ppid}`,
174
+ '$names = @()',
175
+ 'for ($i = 0; $i -lt 6 -and $id; $i++) {',
176
+ ' $process = Get-CimInstance Win32_Process -Filter "ProcessId=$id" -ErrorAction SilentlyContinue',
177
+ ' if (-not $process) { break }',
178
+ ' $names += $process.Name',
179
+ ' $id = $process.ParentProcessId',
180
+ '}',
181
+ '$names -join [Environment]::NewLine',
182
+ ].join('; '),
183
+ ], {
184
+ encoding: 'utf8',
185
+ stdio: ['ignore', 'pipe', 'ignore'],
186
+ windowsHide: true,
187
+ })).split(/\r?\n/));
188
+ }
189
+ catch (_error) {
190
+ // fall back to environment-based detection
191
+ }
192
+ return undefined;
193
+ }
194
+ function cmdAutoRunSegment(managedFile) {
195
+ return `if exist "${managedFile}" call "${managedFile}"`;
196
+ }
197
+ function escapeRegExp(value) {
198
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
199
+ }
200
+ function cmdAutoRunLocation() {
201
+ const overrideFile = String(process.env[CMD_AUTORUN_OVERRIDE_FILE_ENV] ?? '').trim();
202
+ if (overrideFile) {
203
+ return overrideFile;
204
+ }
205
+ return `${CMD_AUTORUN_REGISTRY_KEY}\\${CMD_AUTORUN_REGISTRY_VALUE}`;
206
+ }
207
+ function parseRegistryQueryValue(output, valueName) {
208
+ for (const line of output.split(/\r?\n/)) {
209
+ const match = line.match(new RegExp(`^\\s*${escapeRegExp(valueName)}\\s+REG_\\w+\\s*(.*)$`));
210
+ if (match) {
211
+ return match[1] ?? '';
212
+ }
213
+ }
214
+ return undefined;
215
+ }
216
+ async function readCmdAutoRunValue() {
217
+ const overrideFile = String(process.env[CMD_AUTORUN_OVERRIDE_FILE_ENV] ?? '').trim();
218
+ if (overrideFile) {
219
+ try {
220
+ return await fs.readFile(overrideFile, 'utf8');
221
+ }
222
+ catch (_error) {
223
+ return undefined;
224
+ }
225
+ }
226
+ if (process.platform !== 'win32') {
227
+ return undefined;
228
+ }
229
+ try {
230
+ const { stdout } = await execFileAsync('reg', ['query', CMD_AUTORUN_REGISTRY_KEY, '/v', CMD_AUTORUN_REGISTRY_VALUE], { windowsHide: true });
231
+ return parseRegistryQueryValue(stdout, CMD_AUTORUN_REGISTRY_VALUE);
232
+ }
233
+ catch (_error) {
234
+ return undefined;
235
+ }
236
+ }
237
+ async function writeCmdAutoRunValue(value) {
238
+ const overrideFile = String(process.env[CMD_AUTORUN_OVERRIDE_FILE_ENV] ?? '').trim();
239
+ if (overrideFile) {
240
+ await fs.mkdir(path.dirname(overrideFile), { recursive: true });
241
+ await fs.writeFile(overrideFile, value, 'utf8');
242
+ return;
243
+ }
244
+ if (process.platform !== 'win32') {
245
+ throw new Error('cmd AutoRun is only supported on Windows.');
246
+ }
247
+ await execFileAsync('reg', ['add', CMD_AUTORUN_REGISTRY_KEY, '/v', CMD_AUTORUN_REGISTRY_VALUE, '/t', 'REG_SZ', '/d', value, '/f'], { windowsHide: true });
248
+ }
249
+ async function deleteCmdAutoRunValue() {
250
+ const overrideFile = String(process.env[CMD_AUTORUN_OVERRIDE_FILE_ENV] ?? '').trim();
251
+ if (overrideFile) {
252
+ await fs.rm(overrideFile, { force: true });
253
+ return;
254
+ }
255
+ if (process.platform !== 'win32') {
256
+ return;
257
+ }
258
+ try {
259
+ await execFileAsync('reg', ['delete', CMD_AUTORUN_REGISTRY_KEY, '/v', CMD_AUTORUN_REGISTRY_VALUE, '/f'], {
260
+ windowsHide: true,
261
+ });
262
+ }
263
+ catch (_error) {
264
+ // ignore missing registry value
265
+ }
266
+ }
267
+ function appendCmdAutoRunSegment(currentValue, segment) {
268
+ const current = String(currentValue ?? '').trim();
269
+ if (!current) {
270
+ return segment;
271
+ }
272
+ if (current.includes(segment)) {
273
+ return current;
274
+ }
275
+ return `${current} & ${segment}`;
276
+ }
277
+ function removeCmdAutoRunSegment(currentValue, segment) {
278
+ const current = String(currentValue ?? '').trim();
279
+ if (!current) {
280
+ return '';
281
+ }
282
+ if (current === segment) {
283
+ return '';
284
+ }
285
+ const escapedSegment = escapeRegExp(segment);
286
+ let next = current
287
+ .replace(new RegExp(`\\s*&\\s*${escapedSegment}$`), '')
288
+ .replace(new RegExp(`^${escapedSegment}\\s*&\\s*`), '')
289
+ .replace(new RegExp(`\\s*&\\s*${escapedSegment}(?=\\s*&\\s*)`, 'g'), '');
290
+ next = next.replace(/\s{2,}/g, ' ').trim();
291
+ next = next.replace(/\s*&\s*&\s*/g, ' & ').trim();
292
+ return next;
293
+ }
294
+ async function setupCmdAutoRun(managedFile) {
295
+ const segment = cmdAutoRunSegment(managedFile);
296
+ const currentValue = await readCmdAutoRunValue();
297
+ const nextValue = appendCmdAutoRunSegment(currentValue, segment);
298
+ if (nextValue === String(currentValue ?? '').trim()) {
299
+ return {
300
+ configured: true,
301
+ location: cmdAutoRunLocation(),
302
+ };
303
+ }
304
+ await writeCmdAutoRunValue(nextValue);
305
+ return {
306
+ configured: true,
307
+ location: cmdAutoRunLocation(),
308
+ };
309
+ }
310
+ async function removeCmdAutoRun(managedFile) {
311
+ const segment = cmdAutoRunSegment(managedFile);
312
+ const currentValue = await readCmdAutoRunValue();
313
+ const nextValue = removeCmdAutoRunSegment(currentValue, segment);
314
+ const current = String(currentValue ?? '').trim();
315
+ const changed = nextValue !== current;
316
+ if (changed) {
317
+ if (nextValue) {
318
+ await writeCmdAutoRunValue(nextValue);
319
+ }
320
+ else {
321
+ await deleteCmdAutoRunValue();
322
+ }
323
+ }
324
+ return {
325
+ removed: changed,
326
+ location: cmdAutoRunLocation(),
327
+ };
328
+ }
329
+ export function detectSessionShell() {
330
+ if (process.env.FISH_VERSION) {
331
+ return 'fish';
332
+ }
333
+ if (process.env.ZSH_VERSION) {
334
+ return 'zsh';
335
+ }
336
+ if (process.env.BASH_VERSION) {
337
+ return 'bash';
338
+ }
339
+ const normalizedShell = normalizeShellHint(String(process.env.SHELL ?? ''));
340
+ const normalizedLoginShell = normalizeShellHint(String(process.env.LOGINSHELL ?? ''));
341
+ if (process.platform === 'win32') {
342
+ const isMsysRuntime = Boolean(process.env.MSYSTEM);
343
+ const detectedWindowsShell = detectWindowsShellByParentProcess();
344
+ const shellHint = normalizedLoginShell ?? normalizedShell;
345
+ if (detectedWindowsShell === 'bash' || detectedWindowsShell === 'zsh' || detectedWindowsShell === 'fish') {
346
+ return detectedWindowsShell;
347
+ }
348
+ if (shellHint === 'bash' || shellHint === 'zsh' || shellHint === 'fish') {
349
+ return shellHint;
350
+ }
351
+ if (detectedWindowsShell && !(isMsysRuntime && detectedWindowsShell === 'cmd' && shellHint)) {
352
+ return detectedWindowsShell;
353
+ }
354
+ if (normalizedLoginShell) {
355
+ return normalizedLoginShell;
356
+ }
357
+ if (normalizedShell) {
358
+ return normalizedShell;
359
+ }
360
+ if (isMsysRuntime) {
361
+ return 'bash';
362
+ }
363
+ const comspec = String(process.env.ComSpec ?? '').trim().toLowerCase();
364
+ const prompt = String(process.env.PROMPT ?? '').trim();
365
+ if (prompt && comspec.endsWith('cmd.exe')) {
366
+ return 'cmd';
367
+ }
368
+ if (process.env.PSModulePath) {
369
+ return 'powershell';
370
+ }
371
+ if (comspec.endsWith('cmd.exe')) {
372
+ return 'cmd';
373
+ }
374
+ return undefined;
375
+ }
376
+ if (normalizedShell) {
377
+ return normalizedShell;
378
+ }
379
+ if (normalizedLoginShell) {
380
+ return normalizedLoginShell;
381
+ }
382
+ return undefined;
383
+ }
384
+ export function getSessionShellProfilePaths(shell) {
385
+ const home = resolveSessionHomeDir();
386
+ switch (shell) {
387
+ case 'bash':
388
+ return [path.join(home, '.bashrc')];
389
+ case 'zsh':
390
+ return [path.join(home, '.zshrc')];
391
+ case 'fish':
392
+ return [path.join(home, '.config', 'fish', 'config.fish')];
393
+ case 'powershell':
394
+ return process.platform === 'win32'
395
+ ? detectWindowsPowerShellProfiles()
396
+ : [path.join(home, '.config', 'powershell', 'Microsoft.PowerShell_profile.ps1')];
397
+ case 'cmd':
398
+ return [];
399
+ default:
400
+ return [];
401
+ }
402
+ }
403
+ export function getSessionShellProfilePath(shell) {
404
+ return getSessionShellProfilePaths(shell)[0];
405
+ }
406
+ function buildManagedFileContent(shell) {
407
+ switch (shell) {
408
+ case 'bash':
409
+ case 'zsh':
410
+ return [
411
+ '# NocoBase session integration',
412
+ `export NB_SESSION_ID="nb-$(node -e 'console.log(require("node:crypto").randomUUID())')"`,
413
+ '',
414
+ ].join('\n');
415
+ case 'fish':
416
+ return [
417
+ '# NocoBase session integration',
418
+ 'set -gx NB_SESSION_ID "nb-"(node -e "console.log(require(\'node:crypto\').randomUUID())")',
419
+ '',
420
+ ].join('\n');
421
+ case 'powershell':
422
+ return [
423
+ '# NocoBase session integration',
424
+ ' $env:NB_SESSION_ID = "nb-" + [guid]::NewGuid().ToString()',
425
+ '',
426
+ ].join('\n');
427
+ case 'cmd':
428
+ return [
429
+ '@echo off',
430
+ 'set "NB_SESSION_ID=nb-%RANDOM%%RANDOM%%RANDOM%%RANDOM%"',
431
+ '',
432
+ ].join('\r\n');
433
+ default:
434
+ return '';
435
+ }
436
+ }
437
+ function buildProfileSnippet(shell, managedPath) {
438
+ switch (shell) {
439
+ case 'bash':
440
+ case 'zsh':
441
+ return [
442
+ START_MARKER,
443
+ `[ -f "${managedPath}" ] && source "${managedPath}"`,
444
+ END_MARKER,
445
+ ].join('\n');
446
+ case 'fish':
447
+ return [
448
+ START_MARKER,
449
+ `if test -f '${managedPath.replace(/'/g, "\\'")}'`,
450
+ ` source '${managedPath.replace(/'/g, "\\'")}'`,
451
+ 'end',
452
+ END_MARKER,
453
+ ].join('\n');
454
+ case 'powershell':
455
+ return [
456
+ START_MARKER,
457
+ `if (Test-Path '${managedPath.replace(/'/g, "''")}') { . '${managedPath.replace(/'/g, "''")}' }`,
458
+ END_MARKER,
459
+ ].join('\r\n');
460
+ default:
461
+ return '';
462
+ }
463
+ }
464
+ function buildOpencodePluginContent() {
465
+ return [
466
+ '/**',
467
+ ' * opencode plugin: expose the current conversation session id to shell tools.',
468
+ ' *',
469
+ ' * Semantics:',
470
+ ' * - same chat: stable',
471
+ ' * - different chat: different',
472
+ ' */',
473
+ 'export const NbAgentSessionPlugin = async () => {',
474
+ ' return {',
475
+ ' "shell.env": async (input, output) => {',
476
+ ' const sessionID = typeof input?.sessionID === "string" ? input.sessionID.trim() : "";',
477
+ ' if (!sessionID) {',
478
+ ' return;',
479
+ ' }',
480
+ '',
481
+ ' output.env = {',
482
+ ' ...output.env,',
483
+ ' NB_SESSION_ID: sessionID,',
484
+ ' };',
485
+ ' },',
486
+ ' };',
487
+ '};',
488
+ '',
489
+ 'export default NbAgentSessionPlugin;',
490
+ '',
491
+ ].join('\n');
492
+ }
493
+ async function installOpencodeSessionPlugin(shell) {
494
+ const configDir = opencodeConfigDir(shell);
495
+ const configFile = opencodeConfigFilePath(shell);
496
+ const pluginFile = opencodePluginFilePath(shell);
497
+ const configDirExists = await pathExists(configDir);
498
+ if (!configDirExists) {
499
+ return {
500
+ pluginFile,
501
+ configFile,
502
+ configured: false,
503
+ skippedReason: 'opencode_dir_not_found',
504
+ };
505
+ }
506
+ await fs.mkdir(path.dirname(pluginFile), { recursive: true });
507
+ await fs.writeFile(pluginFile, buildOpencodePluginContent(), 'utf8');
508
+ let config = {};
509
+ try {
510
+ const raw = await fs.readFile(configFile, 'utf8');
511
+ config = JSON.parse(raw);
512
+ }
513
+ catch (_error) {
514
+ config = {
515
+ $schema: 'https://opencode.ai/config.json',
516
+ };
517
+ }
518
+ const plugins = Array.isArray(config.plugin) ? [...config.plugin] : [];
519
+ if (!plugins.includes(pluginFile)) {
520
+ plugins.push(pluginFile);
521
+ }
522
+ config.plugin = plugins;
523
+ await fs.writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
524
+ return {
525
+ pluginFile,
526
+ configFile,
527
+ configured: true,
528
+ };
529
+ }
530
+ async function removeOpencodeSessionPlugin(shell) {
531
+ const pluginFile = opencodePluginFilePath(shell);
532
+ const configFile = opencodeConfigFilePath(shell);
533
+ let pluginFileRemoved = false;
534
+ try {
535
+ await fs.rm(pluginFile, { force: true });
536
+ pluginFileRemoved = true;
537
+ }
538
+ catch (_error) {
539
+ pluginFileRemoved = false;
540
+ }
541
+ let configUpdated = false;
542
+ try {
543
+ const raw = await fs.readFile(configFile, 'utf8');
544
+ const config = JSON.parse(raw);
545
+ if (Array.isArray(config.plugin)) {
546
+ const nextPlugins = config.plugin.filter((item) => item !== pluginFile);
547
+ if (nextPlugins.length !== config.plugin.length) {
548
+ config.plugin = nextPlugins;
549
+ await fs.writeFile(configFile, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
550
+ configUpdated = true;
551
+ }
552
+ }
553
+ }
554
+ catch (_error) {
555
+ configUpdated = false;
556
+ }
557
+ return {
558
+ pluginFile,
559
+ configFile,
560
+ pluginFileRemoved,
561
+ configUpdated,
562
+ };
563
+ }
564
+ async function upsertMarkedBlock(filePath, block) {
565
+ let content = '';
566
+ try {
567
+ content = await fs.readFile(filePath, 'utf8');
568
+ }
569
+ catch (_error) {
570
+ content = '';
571
+ }
572
+ const pattern = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}\\r?\\n?`, 'g');
573
+ const cleaned = content.replace(pattern, '').replace(/\s*$/, '');
574
+ const next = cleaned ? `${cleaned}\n\n${block}\n` : `${block}\n`;
575
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
576
+ await fs.writeFile(filePath, next, 'utf8');
577
+ }
578
+ async function removeMarkedBlock(filePath) {
579
+ try {
580
+ const content = await fs.readFile(filePath, 'utf8');
581
+ const pattern = new RegExp(`${START_MARKER}[\\s\\S]*?${END_MARKER}\\r?\\n?`, 'g');
582
+ const next = content.replace(pattern, '').replace(/\n{3,}/g, '\n\n').trimEnd();
583
+ await fs.writeFile(filePath, next ? `${next}\n` : '', 'utf8');
584
+ return true;
585
+ }
586
+ catch (_error) {
587
+ return false;
588
+ }
589
+ }
590
+ export async function setupSessionIntegration(shell) {
591
+ const managedFile = managedFilePath(shell);
592
+ await fs.mkdir(path.dirname(managedFile), { recursive: true });
593
+ await fs.writeFile(managedFile, buildManagedFileContent(shell), 'utf8');
594
+ const agent = await installOpencodeSessionPlugin(shell);
595
+ if (shell === 'cmd') {
596
+ if (process.platform === 'win32' || String(process.env[CMD_AUTORUN_OVERRIDE_FILE_ENV] ?? '').trim()) {
597
+ try {
598
+ const autoRun = await setupCmdAutoRun(managedFile);
599
+ return {
600
+ shell,
601
+ managedFile,
602
+ profileFiles: [],
603
+ profileUpdated: false,
604
+ cmdAutoRunConfigured: autoRun.configured,
605
+ cmdAutoRunLocation: autoRun.location,
606
+ agentPluginFile: agent.pluginFile,
607
+ agentConfigFile: agent.configFile,
608
+ agentConfigured: agent.configured,
609
+ agentSkippedReason: agent.skippedReason,
610
+ };
611
+ }
612
+ catch (_error) {
613
+ // fall through to the manual step guidance below
614
+ }
615
+ }
616
+ return {
617
+ shell,
618
+ managedFile,
619
+ profileFiles: [],
620
+ profileUpdated: false,
621
+ cmdAutoRunConfigured: false,
622
+ agentPluginFile: agent.pluginFile,
623
+ agentConfigFile: agent.configFile,
624
+ agentConfigured: agent.configured,
625
+ agentSkippedReason: agent.skippedReason,
626
+ manualStep: `cmd.exe AutoRun was not updated. Run 'call "${managedFile}"' in the current cmd session before using nb, or configure AutoRun to call it automatically.`,
627
+ };
628
+ }
629
+ const profileFiles = getSessionShellProfilePaths(shell);
630
+ if (profileFiles.length > 0) {
631
+ const snippet = buildProfileSnippet(shell, managedFile);
632
+ await Promise.all(profileFiles.map((profileFile) => upsertMarkedBlock(profileFile, snippet)));
633
+ return {
634
+ shell,
635
+ managedFile,
636
+ profileFile: profileFiles[0],
637
+ profileFiles,
638
+ profileUpdated: true,
639
+ cmdAutoRunConfigured: false,
640
+ agentPluginFile: agent.pluginFile,
641
+ agentConfigFile: agent.configFile,
642
+ agentConfigured: agent.configured,
643
+ agentSkippedReason: agent.skippedReason,
644
+ };
645
+ }
646
+ return {
647
+ shell,
648
+ managedFile,
649
+ profileFiles: [],
650
+ profileUpdated: false,
651
+ cmdAutoRunConfigured: false,
652
+ agentPluginFile: agent.pluginFile,
653
+ agentConfigFile: agent.configFile,
654
+ agentConfigured: agent.configured,
655
+ agentSkippedReason: agent.skippedReason,
656
+ manualStep: `cmd.exe does not have a shell profile like bash or PowerShell. Run "${managedFile}" in the current cmd session before using nb, or configure AutoRun to call it automatically.`,
657
+ };
658
+ }
659
+ export async function removeSessionIntegration(shell) {
660
+ const managedFile = managedFilePath(shell);
661
+ let managedFileRemoved = false;
662
+ try {
663
+ await fs.rm(managedFile, { force: true });
664
+ managedFileRemoved = true;
665
+ }
666
+ catch (_error) {
667
+ managedFileRemoved = false;
668
+ }
669
+ if (shell === 'cmd') {
670
+ const autoRun = await removeCmdAutoRun(managedFile);
671
+ const agent = await removeOpencodeSessionPlugin(shell);
672
+ return {
673
+ shell,
674
+ managedFile,
675
+ profileFiles: [],
676
+ profileUpdated: false,
677
+ managedFileRemoved,
678
+ cmdAutoRunRemoved: autoRun.removed,
679
+ cmdAutoRunLocation: autoRun.location,
680
+ agentPluginFile: agent.pluginFile,
681
+ agentConfigFile: agent.configFile,
682
+ agentPluginRemoved: agent.pluginFileRemoved,
683
+ agentConfigUpdated: agent.configUpdated,
684
+ };
685
+ }
686
+ const profileFiles = getSessionShellProfilePaths(shell);
687
+ const profileUpdateResults = await Promise.all(profileFiles.map(async (profileFile) => ((await removeMarkedBlock(profileFile)) ? profileFile : undefined)));
688
+ const updatedProfileFiles = profileUpdateResults.filter((profileFile) => Boolean(profileFile));
689
+ const agent = await removeOpencodeSessionPlugin(shell);
690
+ return {
691
+ shell,
692
+ managedFile,
693
+ profileFile: updatedProfileFiles[0],
694
+ profileFiles: updatedProfileFiles,
695
+ profileUpdated: updatedProfileFiles.length > 0,
696
+ managedFileRemoved,
697
+ cmdAutoRunRemoved: false,
698
+ agentPluginFile: agent.pluginFile,
699
+ agentConfigFile: agent.configFile,
700
+ agentPluginRemoved: agent.pluginFileRemoved,
701
+ agentConfigUpdated: agent.configUpdated,
702
+ };
703
+ }