@pixelbyte-software/pixcode 1.38.3 → 1.38.5

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,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const gitRoute = readFileSync('server/routes/git.js', 'utf8');
7
+ const changedFilesHook = readFileSync('src/hooks/useChangedFilesMonitor.ts', 'utf8');
8
+ const mainContent = readFileSync('src/components/main-content/view/MainContent.tsx', 'utf8');
9
+ const quickSettings = readFileSync('src/components/quick-settings-panel/view/QuickSettingsContent.tsx', 'utf8');
10
+
11
+ assert.ok(
12
+ gitRoute.includes('isGitRepository: false'),
13
+ 'Git status route should return a structured non-git status instead of a Git operation failed error.',
14
+ );
15
+
16
+ assert.ok(
17
+ gitRoute.includes('filesystemChangeSnapshots'),
18
+ 'Git status route should maintain filesystem snapshots for local-only projects.',
19
+ );
20
+
21
+ assert.ok(
22
+ changedFilesHook.includes('isGitRepository?: boolean'),
23
+ 'Changed-files monitor should understand git-backed and filesystem-backed status payloads.',
24
+ );
25
+
26
+ assert.ok(
27
+ !changedFilesHook.includes('setError(data.details || data.error'),
28
+ 'Changed-files monitor should not surface the non-git fallback as a blocking error.',
29
+ );
30
+
31
+ assert.ok(
32
+ mainContent.includes('ChangedFilesActivityRail'),
33
+ 'Main content should render changed-file activity beside chat/orchestration, not only in Quick Settings.',
34
+ );
35
+
36
+ assert.ok(
37
+ mainContent.includes('setFocusedChangedFilePath(latestDetectedFile.path)'),
38
+ 'New changes should highlight the activity rail without automatically stealing the user into the Files panel.',
39
+ );
40
+
41
+ assert.ok(
42
+ quickSettings.includes('changedFilesSummary'),
43
+ 'Quick Settings should keep a compact Command Center summary instead of being the main changed-files surface.',
44
+ );
45
+
46
+ console.log('command center non-git smoke passed');
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const externalAccess = readFileSync('server/services/external-access.js', 'utf8');
7
+ const mobileTab = readFileSync('src/components/settings/view/tabs/mobile-settings/MobileSettingsTab.tsx', 'utf8');
8
+
9
+ assert.ok(
10
+ externalAccess.includes('createTunnelInstallHint'),
11
+ 'Tunnel service should attach structured install guidance when cloudflared/ngrok is missing.',
12
+ );
13
+
14
+ assert.ok(
15
+ externalAccess.includes('err.installHint'),
16
+ 'Missing tunnel binary errors should expose installHint to the route/UI.',
17
+ );
18
+
19
+ assert.ok(
20
+ mobileTab.includes('installHint'),
21
+ 'Mobile settings should render tunnel install guidance from the backend response.',
22
+ );
23
+
24
+ assert.ok(
25
+ mobileTab.includes('external?.tunnel.installHint'),
26
+ 'Mobile settings should keep guidance visible from persisted tunnel state.',
27
+ );
28
+
29
+ console.log('mobile tunnel guidance smoke passed');
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { readFileSync } from 'node:fs';
5
+
6
+ const notificationCenter = readFileSync('src/components/notifications/InAppNotificationCenter.tsx', 'utf8');
7
+
8
+ assert.ok(
9
+ notificationCenter.includes('inAppEnabledByPreference'),
10
+ 'In-app notification center should explicitly read notificationPreferences.channels.inApp.',
11
+ );
12
+
13
+ assert.ok(
14
+ notificationCenter.includes('if (!inAppEnabled)'),
15
+ 'In-app notification center should skip storing/opening alerts when the in-app channel is disabled.',
16
+ );
17
+
18
+ assert.ok(
19
+ notificationCenter.indexOf('notifyLocalEventOnce') < notificationCenter.indexOf('if (!inAppEnabled)'),
20
+ 'Desktop/browser notification dispatch should remain independent from the in-app channel guard.',
21
+ );
22
+
23
+ console.log('notification in-app preference smoke passed');
@@ -10,6 +10,150 @@ import { spawnCursor } from '../cursor-cli.js';
10
10
 
11
11
  const router = express.Router();
12
12
  const COMMIT_DIFF_CHARACTER_LIMIT = 500_000;
13
+ const FILESYSTEM_SCAN_MAX_FILES = 5_000;
14
+ const FILESYSTEM_SCAN_MAX_DEPTH = 10;
15
+ const filesystemChangeSnapshots = new Map();
16
+ const FILESYSTEM_SCAN_EXCLUDED_DIRS = new Set([
17
+ '.git',
18
+ '.hg',
19
+ '.svn',
20
+ 'node_modules',
21
+ 'dist',
22
+ 'dist-server',
23
+ 'build',
24
+ '.next',
25
+ '.nuxt',
26
+ '.svelte-kit',
27
+ 'coverage',
28
+ '.turbo',
29
+ '.cache',
30
+ '.pixcode-dev',
31
+ ]);
32
+
33
+ function isNotGitRepositoryMessage(message = '') {
34
+ return message.includes('Not a git repository')
35
+ || message.includes('not a git repository')
36
+ || message.includes('Project directory is not a git repository');
37
+ }
38
+
39
+ function shouldSkipFilesystemEntry(entryName) {
40
+ return FILESYSTEM_SCAN_EXCLUDED_DIRS.has(entryName)
41
+ || entryName.endsWith('.log')
42
+ || entryName === '.DS_Store';
43
+ }
44
+
45
+ function toProjectRelativePath(projectPath, filePath) {
46
+ return path.relative(projectPath, filePath).replace(/\\/g, '/');
47
+ }
48
+
49
+ async function collectFilesystemSnapshot(projectPath) {
50
+ const snapshot = new Map();
51
+ let limitReached = false;
52
+
53
+ async function walk(directoryPath, depth) {
54
+ if (limitReached || depth > FILESYSTEM_SCAN_MAX_DEPTH) {
55
+ return;
56
+ }
57
+
58
+ let entries = [];
59
+ try {
60
+ entries = await fs.readdir(directoryPath, { withFileTypes: true });
61
+ } catch {
62
+ return;
63
+ }
64
+
65
+ for (const entry of entries) {
66
+ if (limitReached || shouldSkipFilesystemEntry(entry.name)) {
67
+ continue;
68
+ }
69
+
70
+ const absolutePath = path.join(directoryPath, entry.name);
71
+
72
+ if (entry.isDirectory()) {
73
+ await walk(absolutePath, depth + 1);
74
+ continue;
75
+ }
76
+
77
+ if (!entry.isFile()) {
78
+ continue;
79
+ }
80
+
81
+ try {
82
+ const stat = await fs.stat(absolutePath);
83
+ snapshot.set(toProjectRelativePath(projectPath, absolutePath), {
84
+ mtimeMs: Math.round(stat.mtimeMs),
85
+ size: stat.size,
86
+ });
87
+ } catch {
88
+ continue;
89
+ }
90
+
91
+ if (snapshot.size >= FILESYSTEM_SCAN_MAX_FILES) {
92
+ limitReached = true;
93
+ break;
94
+ }
95
+ }
96
+ }
97
+
98
+ await walk(projectPath, 0);
99
+ return { snapshot, limitReached };
100
+ }
101
+
102
+ function diffFilesystemSnapshots(previousSnapshot, nextSnapshot) {
103
+ if (!previousSnapshot) {
104
+ return { modified: [], added: [], deleted: [] };
105
+ }
106
+
107
+ const modified = [];
108
+ const added = [];
109
+ const deleted = [];
110
+
111
+ for (const [filePath, nextMeta] of nextSnapshot.entries()) {
112
+ const previousMeta = previousSnapshot.get(filePath);
113
+ if (!previousMeta) {
114
+ added.push(filePath);
115
+ continue;
116
+ }
117
+
118
+ if (previousMeta.mtimeMs !== nextMeta.mtimeMs || previousMeta.size !== nextMeta.size) {
119
+ modified.push(filePath);
120
+ }
121
+ }
122
+
123
+ for (const filePath of previousSnapshot.keys()) {
124
+ if (!nextSnapshot.has(filePath)) {
125
+ deleted.push(filePath);
126
+ }
127
+ }
128
+
129
+ return {
130
+ modified: modified.sort(),
131
+ added: added.sort(),
132
+ deleted: deleted.sort(),
133
+ };
134
+ }
135
+
136
+ async function buildFilesystemStatus(projectPath) {
137
+ const normalizedProjectPath = path.resolve(projectPath);
138
+ const previousSnapshot = filesystemChangeSnapshots.get(normalizedProjectPath) ?? null;
139
+ const { snapshot, limitReached } = await collectFilesystemSnapshot(normalizedProjectPath);
140
+ filesystemChangeSnapshots.set(normalizedProjectPath, snapshot);
141
+ const { modified, added, deleted } = diffFilesystemSnapshots(previousSnapshot, snapshot);
142
+
143
+ return {
144
+ isGitRepository: false,
145
+ trackingMode: 'filesystem',
146
+ branch: null,
147
+ hasCommits: false,
148
+ modified,
149
+ added,
150
+ deleted,
151
+ untracked: [],
152
+ snapshotReady: Boolean(previousSnapshot),
153
+ fileCount: snapshot.size,
154
+ scanLimitReached: limitReached,
155
+ };
156
+ }
13
157
 
14
158
  function spawnAsync(command, args, options = {}) {
15
159
  return new Promise((resolve, reject) => {
@@ -297,8 +441,9 @@ router.get('/status', async (req, res) => {
297
441
  return res.status(400).json({ error: 'Project name is required' });
298
442
  }
299
443
 
444
+ let projectPath;
300
445
  try {
301
- const projectPath = await getActualProjectPath(project);
446
+ projectPath = await getActualProjectPath(project);
302
447
 
303
448
  // Validate git repository
304
449
  await validateGitRepository(projectPath);
@@ -340,6 +485,15 @@ router.get('/status', async (req, res) => {
340
485
  untracked
341
486
  });
342
487
  } catch (error) {
488
+ if (projectPath && isNotGitRepositoryMessage(error.message)) {
489
+ try {
490
+ res.json(await buildFilesystemStatus(projectPath));
491
+ return;
492
+ } catch (fallbackError) {
493
+ console.error('Filesystem status fallback error:', fallbackError);
494
+ }
495
+ }
496
+
343
497
  console.error('Git status error:', error);
344
498
  res.json({
345
499
  error: error.message.includes('not a git repository') || error.message.includes('Project directory is not a git repository')
@@ -104,7 +104,11 @@ router.post('/tunnel', async (req, res) => {
104
104
  // required external binary is missing" — it tells the UI to show the
105
105
  // "install cloudflared/ngrok" hint rather than a generic server error.
106
106
  const status = error?.code === 'ENOENT_TUNNEL' ? 424 : 502;
107
- res.status(status).json({ error: error?.message || 'Tunnel start failed', tunnel: getTunnelState() });
107
+ res.status(status).json({
108
+ error: error?.message || 'Tunnel start failed',
109
+ installHint: error?.installHint,
110
+ tunnel: getTunnelState(),
111
+ });
108
112
  }
109
113
  });
110
114
 
@@ -37,6 +37,7 @@ let tunnelState = {
37
37
  binary: null, // 'cloudflared' | 'ngrok'
38
38
  url: null,
39
39
  error: null,
40
+ installHint: null,
40
41
  log: [],
41
42
  };
42
43
 
@@ -66,6 +67,18 @@ const detectBinary = async () => {
66
67
  return null;
67
68
  };
68
69
 
70
+ const createTunnelInstallHint = () => ({
71
+ title: 'Tunnel binary required',
72
+ message: 'Install cloudflared or ngrok to create a public mobile URL. Local LAN QR codes still work on the same Wi-Fi/network.',
73
+ commands: [
74
+ 'macOS: brew install cloudflared',
75
+ 'Windows: winget install Cloudflare.cloudflared',
76
+ 'Linux: install cloudflared from https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/',
77
+ 'Alternative: install and authenticate ngrok from https://ngrok.com/download',
78
+ ],
79
+ docsUrl: 'https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/',
80
+ });
81
+
69
82
  const cloudflareUrlRegex = /https?:\/\/[a-z0-9.-]+trycloudflare\.com/i;
70
83
  const ngrokUrlRegex = /https?:\/\/[a-z0-9.-]+\.ngrok(-free)?\.(app|io)/i;
71
84
 
@@ -90,16 +103,18 @@ export const startTunnel = async ({ port }) => {
90
103
 
91
104
  const binary = await detectBinary();
92
105
  if (!binary) {
93
- tunnelState = { running: false, binary: null, url: null, error: 'No tunnel binary found', log: [] };
106
+ const installHint = createTunnelInstallHint();
107
+ tunnelState = { running: false, binary: null, url: null, error: 'No tunnel binary found', installHint, log: [] };
94
108
  const err = new Error('No tunnel binary found (tried cloudflared, ngrok)');
95
109
  err.code = 'ENOENT_TUNNEL';
110
+ err.installHint = installHint;
96
111
  throw err;
97
112
  }
98
113
 
99
114
  const args = buildTunnelArgs(binary, port);
100
115
  const child = spawn(binary, args, { stdio: ['ignore', 'pipe', 'pipe'] });
101
116
  tunnelProc = child;
102
- tunnelState = { running: true, binary, url: null, error: null, log: [] };
117
+ tunnelState = { running: true, binary, url: null, error: null, installHint: null, log: [] };
103
118
 
104
119
  const handleChunk = (chunk) => {
105
120
  const text = chunk.toString();
@@ -119,6 +134,7 @@ export const startTunnel = async ({ port }) => {
119
134
  binary,
120
135
  url: null,
121
136
  error: code === 0 ? null : `Tunnel exited with code ${code}`,
137
+ installHint: null,
122
138
  log: tunnelState.log,
123
139
  };
124
140
  });
@@ -138,7 +154,7 @@ export const startTunnel = async ({ port }) => {
138
154
  // If we never captured a URL, kill the child so we don't leak it.
139
155
  try { child.kill(); } catch { /* ignore */ }
140
156
  tunnelProc = null;
141
- tunnelState = { ...tunnelState, running: false, error: 'Tunnel did not report a public URL' };
157
+ tunnelState = { ...tunnelState, running: false, error: 'Tunnel did not report a public URL', installHint: null };
142
158
  throw new Error(tunnelState.error);
143
159
  }
144
160
 
@@ -147,7 +163,7 @@ export const startTunnel = async ({ port }) => {
147
163
 
148
164
  export const stopTunnel = async () => {
149
165
  if (!tunnelProc) {
150
- tunnelState = { running: false, binary: null, url: null, error: null, log: [] };
166
+ tunnelState = { running: false, binary: null, url: null, error: null, installHint: null, log: [] };
151
167
  return tunnelState;
152
168
  }
153
169
  try {
@@ -156,7 +172,7 @@ export const stopTunnel = async () => {
156
172
  // already dead
157
173
  }
158
174
  tunnelProc = null;
159
- tunnelState = { running: false, binary: null, url: null, error: null, log: [] };
175
+ tunnelState = { running: false, binary: null, url: null, error: null, installHint: null, log: [] };
160
176
  return tunnelState;
161
177
  };
162
178