@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.
- package/dist/assets/index-B-OgjpDF.css +32 -0
- package/dist/assets/{index-Cc0uLPZw.js → index-DHC6THrb.js} +174 -170
- package/dist/index.html +2 -2
- package/dist-server/server/routes/git.js +136 -1
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/network.js +5 -1
- package/dist-server/server/routes/network.js.map +1 -1
- package/dist-server/server/services/external-access.js +20 -5
- package/dist-server/server/services/external-access.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/command-center-agent-writes.mjs +51 -0
- package/scripts/smoke/command-center-non-git.mjs +46 -0
- package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -0
- package/scripts/smoke/notification-inapp-preference.mjs +23 -0
- package/server/routes/git.js +155 -1
- package/server/routes/network.js +5 -1
- package/server/services/external-access.js +21 -5
- package/dist/assets/index-BzgMq98S.css +0 -32
|
@@ -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');
|
package/server/routes/git.js
CHANGED
|
@@ -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
|
-
|
|
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')
|
package/server/routes/network.js
CHANGED
|
@@ -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({
|
|
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
|
-
|
|
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
|
|