@pixelbyte-software/pixcode 1.48.6 → 1.49.1

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.
@@ -65,20 +65,51 @@ assert.match(
65
65
 
66
66
  assert.match(
67
67
  updater,
68
- /npm[\s\S]*install[\s\S]*--no-audit[\s\S]*--no-fund/,
69
- 'Safe updater should reinstall dependencies after updating source files.',
68
+ /shouldRunNpmInstall/,
69
+ 'Safe updater should decide whether dependency reconciliation is needed from changed files.',
70
70
  );
71
71
 
72
72
  assert.match(
73
73
  updater,
74
- /npm[\s\S]*run[\s\S]*build/,
75
- 'Safe updater should rebuild source installs after updating source files.',
74
+ /Dependencies unchanged; skipping npm install\./,
75
+ 'Safe updater should skip npm install when package manifests did not change.',
76
76
  );
77
77
 
78
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'pixcode-git-update-'));
79
- const origin = path.join(tempRoot, 'origin.git');
80
- const source = path.join(tempRoot, 'source');
81
- const install = path.join(tempRoot, 'install');
78
+ assert.match(
79
+ updater,
80
+ /shouldRunBuild/,
81
+ 'Safe updater should decide whether source rebuild is needed from changed files.',
82
+ );
83
+
84
+ assert.match(
85
+ updater,
86
+ /Build inputs unchanged; skipping build\./,
87
+ 'Safe updater should skip build when only non-runtime files changed.',
88
+ );
89
+
90
+ function makeTempRepo(name) {
91
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), `pixcode-git-update-${name}-`));
92
+ const origin = path.join(tempRoot, 'origin.git');
93
+ const source = path.join(tempRoot, 'source');
94
+ const install = path.join(tempRoot, 'install');
95
+
96
+ fs.mkdirSync(source, { recursive: true });
97
+ run('git', ['init', '--bare', origin], tempRoot);
98
+ run('git', ['init', '-b', 'main'], source);
99
+ writePackage(source, '1.0.0');
100
+ fs.mkdirSync(path.join(source, 'src'), { recursive: true });
101
+ fs.writeFileSync(path.join(source, 'src', 'app.js'), 'old\n');
102
+ fs.writeFileSync(path.join(source, 'README.md'), 'old docs\n');
103
+ fs.writeFileSync(path.join(source, 'tracked.txt'), 'old\n');
104
+ run('git', ['add', '.'], source);
105
+ run('git', ['commit', '-m', 'initial'], source);
106
+ run('git', ['remote', 'add', 'origin', origin], source);
107
+ run('git', ['push', '-u', 'origin', 'main'], source);
108
+ run('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], origin);
109
+ run('git', ['clone', origin, install], tempRoot);
110
+
111
+ return { origin, source, install };
112
+ }
82
113
 
83
114
  function run(command, args, cwd) {
84
115
  const result = spawnSync(command, args, {
@@ -99,22 +130,24 @@ function run(command, args, cwd) {
99
130
  `${command} ${args.join(' ')} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
100
131
  );
101
132
 
102
- return result.stdout.trim();
133
+ return `${result.stdout}${result.stderr}`.trim();
103
134
  }
104
135
 
105
- function writePackage(version) {
136
+ function writePackage(root, version, dependencies = {}) {
106
137
  fs.writeFileSync(
107
- path.join(source, 'package.json'),
138
+ path.join(root, 'package.json'),
108
139
  JSON.stringify({
109
140
  name: 'pixcode-update-smoke',
110
141
  version,
111
142
  scripts: {
143
+ preinstall: 'node -e "require(\\"node:fs\\").writeFileSync(\\"install-ran.txt\\", \\"install\\")"',
112
144
  build: 'node -e "require(\\"node:fs\\").writeFileSync(\\"built.txt\\", \\"built\\")"',
113
145
  },
146
+ dependencies,
114
147
  }, null, 2),
115
148
  );
116
149
  fs.writeFileSync(
117
- path.join(source, 'package-lock.json'),
150
+ path.join(root, 'package-lock.json'),
118
151
  JSON.stringify({
119
152
  name: 'pixcode-update-smoke',
120
153
  version,
@@ -130,47 +163,93 @@ function writePackage(version) {
130
163
  );
131
164
  }
132
165
 
133
- fs.mkdirSync(source, { recursive: true });
134
- run('git', ['init', '--bare', origin], tempRoot);
135
- run('git', ['init', '-b', 'main'], source);
136
- writePackage('1.0.0');
137
- fs.writeFileSync(path.join(source, 'tracked.txt'), 'old\n');
138
- run('git', ['add', '.'], source);
139
- run('git', ['commit', '-m', 'initial'], source);
140
- run('git', ['remote', 'add', 'origin', origin], source);
141
- run('git', ['push', '-u', 'origin', 'main'], source);
142
- run('git', ['symbolic-ref', 'HEAD', 'refs/heads/main'], origin);
143
- run('git', ['clone', origin, install], tempRoot);
144
-
145
- writePackage('1.0.1');
146
- fs.writeFileSync(path.join(source, 'tracked.txt'), 'new\n');
147
- run('git', ['add', '.'], source);
148
- run('git', ['commit', '-m', 'update'], source);
149
- run('git', ['push', 'origin', 'main'], source);
150
-
151
- fs.writeFileSync(path.join(install, 'tracked.txt'), 'local dirty change\n');
152
- fs.writeFileSync(path.join(install, 'untracked.txt'), 'local untracked change\n');
153
- run(process.execPath, [updaterPath], install);
154
-
155
- assert.equal(
156
- JSON.parse(fs.readFileSync(path.join(install, 'package.json'), 'utf8')).version,
157
- '1.0.1',
158
- 'Safe updater should fast-forward the install checkout.',
159
- );
160
- assert.equal(
161
- fs.readFileSync(path.join(install, 'tracked.txt'), 'utf8'),
162
- 'new\n',
163
- 'Safe updater should apply the remote tracked file after stashing local edits.',
164
- );
165
- assert.match(
166
- run('git', ['stash', 'list'], install),
167
- /pixcode-auto-update-/,
168
- 'Safe updater should leave local dirty files recoverable in git stash.',
169
- );
170
- assert.equal(
171
- fs.readFileSync(path.join(install, 'built.txt'), 'utf8'),
172
- 'built',
173
- 'Safe updater should run the repository build after installing dependencies.',
174
- );
166
+ {
167
+ const { source, install } = makeTempRepo('deps');
168
+
169
+ fs.mkdirSync(path.join(source, 'local-dep'), { recursive: true });
170
+ fs.writeFileSync(
171
+ path.join(source, 'local-dep', 'package.json'),
172
+ JSON.stringify({ name: 'pixcode-smoke-local-dep', version: '1.0.0' }, null, 2),
173
+ );
174
+ writePackage(source, '1.0.1', { 'pixcode-smoke-local-dep': 'file:./local-dep' });
175
+ fs.writeFileSync(path.join(source, 'tracked.txt'), 'new\n');
176
+ run('git', ['add', '.'], source);
177
+ run('git', ['commit', '-m', 'dependency update'], source);
178
+ run('git', ['push', 'origin', 'main'], source);
179
+
180
+ fs.writeFileSync(path.join(install, 'tracked.txt'), 'local dirty change\n');
181
+ fs.writeFileSync(path.join(install, 'untracked.txt'), 'local untracked change\n');
182
+ run(process.execPath, [updaterPath], install);
183
+
184
+ assert.equal(
185
+ JSON.parse(fs.readFileSync(path.join(install, 'package.json'), 'utf8')).version,
186
+ '1.0.1',
187
+ 'Safe updater should fast-forward the install checkout.',
188
+ );
189
+ assert.equal(
190
+ fs.readFileSync(path.join(install, 'tracked.txt'), 'utf8'),
191
+ 'new\n',
192
+ 'Safe updater should apply the remote tracked file after stashing local edits.',
193
+ );
194
+ assert.match(
195
+ run('git', ['stash', 'list'], install),
196
+ /pixcode-auto-update-/,
197
+ 'Safe updater should leave local dirty files recoverable in git stash.',
198
+ );
199
+ assert.equal(
200
+ fs.readFileSync(path.join(install, 'install-ran.txt'), 'utf8'),
201
+ 'install',
202
+ 'Dependency updates should run npm install.',
203
+ );
204
+ assert.equal(
205
+ fs.readFileSync(path.join(install, 'built.txt'), 'utf8'),
206
+ 'built',
207
+ 'Safe updater should run the repository build after dependency updates.',
208
+ );
209
+ }
210
+
211
+ {
212
+ const { source, install } = makeTempRepo('source');
213
+
214
+ fs.writeFileSync(path.join(source, 'src', 'app.js'), 'new source\n');
215
+ run('git', ['add', '.'], source);
216
+ run('git', ['commit', '-m', 'source update'], source);
217
+ run('git', ['push', 'origin', 'main'], source);
218
+
219
+ run(process.execPath, [updaterPath], install);
220
+
221
+ assert.equal(
222
+ fs.existsSync(path.join(install, 'install-ran.txt')),
223
+ false,
224
+ 'Source-only updates should skip npm install.',
225
+ );
226
+ assert.equal(
227
+ fs.readFileSync(path.join(install, 'built.txt'), 'utf8'),
228
+ 'built',
229
+ 'Source-only updates should produce a fresh build output.',
230
+ );
231
+ }
232
+
233
+ {
234
+ const { source, install } = makeTempRepo('docs');
235
+
236
+ fs.writeFileSync(path.join(source, 'README.md'), 'new docs\n');
237
+ run('git', ['add', '.'], source);
238
+ run('git', ['commit', '-m', 'docs update'], source);
239
+ run('git', ['push', 'origin', 'main'], source);
240
+
241
+ run(process.execPath, [updaterPath], install);
242
+
243
+ assert.equal(
244
+ fs.existsSync(path.join(install, 'install-ran.txt')),
245
+ false,
246
+ 'Docs-only updates should skip npm install.',
247
+ );
248
+ assert.equal(
249
+ fs.existsSync(path.join(install, 'built.txt')),
250
+ false,
251
+ 'Docs-only updates should not create build output.',
252
+ );
253
+ }
175
254
 
176
255
  console.log('git install update smoke passed');
@@ -17,7 +17,12 @@ const app = read('src/App.tsx');
17
17
  const serverIndex = read('server/index.js');
18
18
  const hermesRoutes = read('server/modules/orchestration/hermes/hermes.routes.ts');
19
19
  const shellTerminal = read('src/components/shell/hooks/useShellTerminal.ts');
20
+ const shellConnection = read('src/components/shell/hooks/useShellConnection.ts');
21
+ const geminiCli = read('server/gemini-cli.js');
22
+ const qwenCli = read('server/qwen-code-cli.js');
23
+ const agentSettings = read('src/components/settings/view/tabs/agents-settings/AgentsSettingsTab.tsx');
20
24
  const gitPanelHeader = read('src/components/git-panel/view/GitPanelHeader.tsx');
25
+ const themeContext = read('src/contexts/ThemeContext.jsx');
21
26
 
22
27
  assert.match(
23
28
  preferenceHook,
@@ -52,13 +57,30 @@ assert.match(workbench, /onClose=\{closeTerminal\}/, 'Closing the workbench term
52
57
  assert.match(workbench, /WorkbenchCliPanelToolbar/, 'CLI terminal should keep history and new-session actions visible.');
53
58
  assert.match(workbench, /WORKBENCH_CLI_STATE_STORAGE_KEY/, 'CLI terminal should remember per-project open state across workspace switches.');
54
59
  assert.match(workbench, /function WorkbenchBottomTerminal/, 'Terminal activity should render as a bottom plain-shell panel.');
60
+ assert.match(workbench, /BOTTOM_TERMINAL_MIN_HEIGHT/, 'Bottom terminal should support height resizing.');
61
+ assert.match(workbench, /isBottomTerminalMinimized/, 'Bottom terminal should support minimizing without closing.');
55
62
  assert.match(workbench, /isPlainShell/, 'Bottom terminal should open the selected project folder without starting the selected AI CLI.');
56
- assert.match(workbench, /startHermesAgent/, 'Right CLI panel should launch Hermes Agent in the active project.');
63
+ assert.match(workbench, /HERMES_AGENT_START_COMMAND/, 'Hermes Agent should launch from the bottom terminal through a server-side sentinel.');
64
+ assert.doesNotMatch(workbench, /Project-scoped agent terminal\. Installs Hermes when missing/, 'Right CLI panel should not show the old Hermes card.');
65
+ assert.doesNotMatch(workbench, /vscodeWorkbench\.hermes\.docsShort|HERMES_AGENT_DOCS_URL/, 'Hermes terminal header should not include a docs shortcut.');
66
+ assert.match(workbench, /shrinkCliPanel/, 'Right CLI panel should expose a shrink action.');
67
+ assert.match(workbench, /expandCliPanel/, 'Right CLI panel should expose an expand action.');
68
+ assert.match(workbench, /vscodeWorkbench\.welcome\.openProject/, 'Workbench welcome should expose a simple Open Project action.');
69
+ assert.match(workbench, /vscodeWorkbench\.welcome\.cloneProject/, 'Workbench welcome should expose a simple Clone action.');
70
+ assert.match(workbench, /vscodeWorkbench\.welcome\.startHermes/, 'Workbench welcome should expose a Hermes start action.');
71
+ assert.match(workbench, /DarkModeToggle/, 'Workbench welcome should expose a dark-mode toggle.');
72
+ assert.match(themeContext, /return true;/, 'Pixcode should default new installs to dark mode.');
57
73
  assert.match(workbench, /openNewCliSessionPicker/, 'CLI terminal plus should return to provider selection before starting a fresh session.');
58
74
  assert.match(workbench, /terminateCurrentCliSession\(selectedProvider\)/, 'CLI terminal plus should terminate the existing provider PTY before showing selection.');
59
75
  assert.match(workbench, /forceNewSession=\{terminalLaunch\.forceNewSession\}/, 'Fresh CLI sessions should bypass the cached default PTY.');
60
76
  assert.match(serverIndex, /\/api\/shell\/sessions\/terminate/, 'Backend should expose an authenticated endpoint to terminate cached provider PTYs immediately.');
61
77
  assert.match(serverIndex, /isPlainShell && !initialCommand/, 'Backend should spawn an interactive plain shell when no terminal command is provided.');
78
+ assert.match(serverIndex, /pixcode:hermes:start/, 'Backend should expand Hermes terminal sentinels on the server host.');
79
+ assert.doesNotMatch(serverIndex, /iex \(irm https:\/\/raw\.githubusercontent\.com\/NousResearch\/hermes-agent\/main\/scripts\/install\.ps1\)/, 'Windows Hermes install should avoid the old inline iex pattern.');
80
+ assert.doesNotMatch(serverIndex, /scriptblock\]::Create\(\(irm https:\/\/raw\.githubusercontent\.com\/NousResearch\/hermes-agent\/main\/scripts\/install\.ps1\)\)/, 'Windows Hermes install should avoid scriptblock Invoke-RestMethod eval patterns.');
81
+ assert.match(serverIndex, /Invoke-WebRequest[\s\S]+install\.ps1[\s\S]+-OutFile/, 'Windows Hermes install should download the installer to a file before running it.');
82
+ assert.match(serverIndex, /Resolve-HermesCommand|resolveHermesCommand/, 'Hermes start/install should resolve an existing hermes binary before installing.');
83
+ assert.match(serverIndex, /buildProviderShellCommand/, 'Provider terminal launch should centralize provider-specific permission flags.');
62
84
  assert.doesNotMatch(shellTerminal, /new WebglAddon\(\)/, 'Workbench terminal should use the stable xterm renderer.');
63
85
  assert.match(workbench, /setActivityPanel\('explorer'\)/, 'Selecting a project should return the side panel to Explorer.');
64
86
  assert.match(gitPanelHeader, /compact/, 'Workbench Source Control should have compact icon-only controls.');
@@ -80,7 +102,20 @@ assert.doesNotMatch(workbench, /tabs\.orchestration/, 'Workbench menus should no
80
102
  assert.match(serverIndex, /app\.use\('\/hermes', createHermesTaskRouter\(\)\)/, 'Internal task router should be mounted behind Hermes.');
81
103
  assert.doesNotMatch(serverIndex, /app\.use\('\/a2a'/, 'Server should not expose the old A2A route.');
82
104
  assert.match(hermesRoutes, /createHermesRouter/, 'Hermes should have a dedicated orchestration API router.');
105
+ assert.match(hermesRoutes, /terminal-launches/, 'Hermes MCP should be able to request visible Pixcode CLI terminal launches.');
106
+ assert.match(hermesRoutes, /install-status/, 'Hermes settings and terminal UI should have an install-status endpoint.');
83
107
  assert.match(serverIndex, /forceNewSession/, 'Shell backend should support explicit fresh-session launches from the workbench.');
84
108
  assert.match(serverIndex, /killProviderPtySessions/, 'Shell backend should terminate old provider PTYs when a fresh CLI session is requested.');
85
109
 
110
+ assert.match(settingsTypes, /'hermes'/, 'Settings Agents should support Hermes Agent as a first-class agent.');
111
+ assert.match(agentSettings, /'hermes'/, 'Settings Agents should list Hermes Agent.');
112
+ assert.match(workbench, /hermesInstallStatus/, 'Workbench should hide Hermes install actions when Hermes is already installed.');
113
+ assert.match(shellConnection, /cursor-tools-settings/, 'Cursor shell launches should read Cursor permission settings, not Claude settings.');
114
+ assert.match(shellConnection, /permissionMode/, 'Shell websocket init should send provider permission mode to the backend.');
115
+ assert.match(serverIndex, /--dangerously-bypass-approvals-and-sandbox/, 'Codex terminal bypass mode should use the Codex CLI bypass flag.');
116
+ assert.match(serverIndex, /--yolo/, 'Gemini and Qwen terminal bypass mode should use --yolo.');
117
+ assert.match(serverIndex, /--dangerously-skip-permissions/, 'Claude/OpenCode terminal bypass mode should pass the provider bypass flag.');
118
+ assert.match(geminiCli, /permissionMode === 'bypassPermissions'[\s\S]+--yolo|--yolo[\s\S]+permissionMode === 'bypassPermissions'/, 'Gemini chat route should map Pixcode bypassPermissions to --yolo.');
119
+ assert.match(qwenCli, /permissionMode === 'bypassPermissions'[\s\S]+--yolo|--yolo[\s\S]+permissionMode === 'bypassPermissions'/, 'Qwen chat route should map Pixcode bypassPermissions to --yolo.');
120
+
86
121
  console.log('pixcode workbench 1.48 smoke passed');
@@ -51,6 +51,15 @@ assert.match(
51
51
  'Workbench center should show a project landing page instead of a blank editor when no project is selected.',
52
52
  );
53
53
 
54
+ for (const token of [
55
+ 'vscodeWorkbench.welcome.openProject',
56
+ 'vscodeWorkbench.welcome.cloneProject',
57
+ 'vscodeWorkbench.welcome.startHermes',
58
+ 'DarkModeToggle',
59
+ ]) {
60
+ assert.match(workbench, new RegExp(token.replaceAll('.', '\\.')), `Workbench welcome should include ${token}.`);
61
+ }
62
+
54
63
  assert.match(
55
64
  workbench,
56
65
  /function WorkbenchCliPanel/,
@@ -244,6 +253,10 @@ assert.match(
244
253
  'Terminal activity should open a VS Code-style bottom terminal instead of the provider CLI picker.',
245
254
  );
246
255
 
256
+ for (const token of ['BOTTOM_TERMINAL_MIN_HEIGHT', 'isBottomTerminalMinimized', 'shrinkCliPanel', 'expandCliPanel']) {
257
+ assert.match(workbench, new RegExp(token), `Workbench should include ${token}.`);
258
+ }
259
+
247
260
  assert.match(
248
261
  workbench,
249
262
  /isPlainShell/,
@@ -252,8 +265,14 @@ assert.match(
252
265
 
253
266
  assert.match(
254
267
  workbench,
255
- /startHermesAgent/,
256
- 'Right CLI panel should offer Hermes Agent as a project-scoped control agent.',
268
+ /HERMES_AGENT_START_COMMAND/,
269
+ 'Hermes Agent should launch through the bottom terminal with a server-side command sentinel.',
270
+ );
271
+
272
+ assert.doesNotMatch(
273
+ workbench,
274
+ /Project-scoped agent terminal\. Installs Hermes when missing/,
275
+ 'Right CLI picker should not show the old Hermes install card.',
257
276
  );
258
277
 
259
278
  assert.match(
@@ -7,6 +7,28 @@ const repoRoot = process.cwd();
7
7
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
8
8
  const stashMessage = `pixcode-auto-update-${timestamp}`;
9
9
  const backupBranch = `pixcode-backup-before-update-${timestamp}`;
10
+ const dependencyManifestFiles = new Set([
11
+ 'package.json',
12
+ 'package-lock.json',
13
+ 'npm-shrinkwrap.json',
14
+ ]);
15
+ const buildInputFiles = new Set([
16
+ 'index.html',
17
+ 'tsconfig.json',
18
+ 'vite.config.js',
19
+ 'vite.config.ts',
20
+ 'tailwind.config.js',
21
+ 'tailwind.config.ts',
22
+ 'postcss.config.js',
23
+ 'postcss.config.cjs',
24
+ 'server/tsconfig.json',
25
+ ]);
26
+ const buildInputPrefixes = [
27
+ 'src/',
28
+ 'server/',
29
+ 'shared/',
30
+ 'public/',
31
+ ];
10
32
 
11
33
  function log(message) {
12
34
  process.stdout.write(`${message}\n`);
@@ -63,6 +85,113 @@ async function getOutput(command, args, options = {}) {
63
85
  return result.stdout.trim();
64
86
  }
65
87
 
88
+ function normalizeGitPath(filePath) {
89
+ return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
90
+ }
91
+
92
+ function readJson(relativePath) {
93
+ try {
94
+ return JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'));
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ function dependencySignature(packageJson) {
101
+ if (!packageJson || typeof packageJson !== 'object') return null;
102
+
103
+ return JSON.stringify({
104
+ dependencies: packageJson.dependencies || {},
105
+ devDependencies: packageJson.devDependencies || {},
106
+ optionalDependencies: packageJson.optionalDependencies || {},
107
+ peerDependencies: packageJson.peerDependencies || {},
108
+ bundledDependencies: packageJson.bundledDependencies || packageJson.bundleDependencies || [],
109
+ });
110
+ }
111
+
112
+ function lockfileSignature(lockfile) {
113
+ if (!lockfile || typeof lockfile !== 'object') return null;
114
+
115
+ const normalizedPackages = {};
116
+ if (lockfile.packages && typeof lockfile.packages === 'object') {
117
+ for (const [packagePath, packageInfo] of Object.entries(lockfile.packages)) {
118
+ if (!packageInfo || typeof packageInfo !== 'object') {
119
+ normalizedPackages[packagePath] = packageInfo;
120
+ continue;
121
+ }
122
+
123
+ if (packagePath === '') {
124
+ const {
125
+ version: _version,
126
+ ...rootPackageInfo
127
+ } = packageInfo;
128
+ normalizedPackages[packagePath] = rootPackageInfo;
129
+ } else {
130
+ normalizedPackages[packagePath] = packageInfo;
131
+ }
132
+ }
133
+ }
134
+
135
+ return JSON.stringify({
136
+ dependencies: lockfile.dependencies || {},
137
+ packages: normalizedPackages,
138
+ });
139
+ }
140
+
141
+ function shouldRunNpmInstall(changedFiles, previousPackageJson, nextPackageJson, previousLockfile, nextLockfile) {
142
+ const changedManifest = changedFiles.some((filePath) => dependencyManifestFiles.has(filePath));
143
+ if (!changedManifest) return false;
144
+
145
+ if (!previousPackageJson || !nextPackageJson) return true;
146
+
147
+ if (dependencySignature(previousPackageJson) !== dependencySignature(nextPackageJson)) {
148
+ return true;
149
+ }
150
+
151
+ if (changedFiles.some((filePath) => filePath === 'package-lock.json' || filePath === 'npm-shrinkwrap.json')) {
152
+ return lockfileSignature(previousLockfile) !== lockfileSignature(nextLockfile);
153
+ }
154
+
155
+ return false;
156
+ }
157
+
158
+ function shouldRunBuild(changedFiles, previousPackageJson, nextPackageJson, installNeeded) {
159
+ if (!nextPackageJson?.scripts?.build) return false;
160
+ if (installNeeded) return true;
161
+
162
+ if (previousPackageJson?.scripts?.build !== nextPackageJson.scripts.build) {
163
+ return true;
164
+ }
165
+
166
+ return changedFiles.some((filePath) => (
167
+ buildInputFiles.has(filePath)
168
+ || buildInputPrefixes.some((prefix) => filePath.startsWith(prefix))
169
+ ));
170
+ }
171
+
172
+ async function getChangedFiles(fromRef, toRef) {
173
+ const output = await getOutput('git', ['diff', '--name-only', fromRef, toRef]);
174
+ return output
175
+ .split('\n')
176
+ .map((filePath) => normalizeGitPath(filePath.trim()))
177
+ .filter(Boolean);
178
+ }
179
+
180
+ function logChangedFiles(changedFiles) {
181
+ if (changedFiles.length === 0) {
182
+ log('Changed files: none.');
183
+ return;
184
+ }
185
+
186
+ log(`Changed files: ${changedFiles.length}.`);
187
+ for (const filePath of changedFiles.slice(0, 25)) {
188
+ log(` - ${filePath}`);
189
+ }
190
+ if (changedFiles.length > 25) {
191
+ log(` ... and ${changedFiles.length - 25} more`);
192
+ }
193
+ }
194
+
66
195
  async function main() {
67
196
  if (!fs.existsSync(path.join(repoRoot, '.git'))) {
68
197
  throw new Error(`Git metadata not found in ${repoRoot}`);
@@ -95,9 +224,20 @@ async function main() {
95
224
  }
96
225
 
97
226
  const checkoutMain = await run('git', ['checkout', 'main'], { allowFailure: true, collectOutput: true });
227
+ let changedFiles = [];
228
+ let previousPackageJson = null;
229
+ let previousLockfile = null;
230
+
98
231
  if (checkoutMain.code !== 0) {
99
232
  log('Local main branch checkout failed; recreating main from origin/main.');
100
233
  await run('git', ['checkout', '-B', 'main', 'origin/main']);
234
+ changedFiles = ['package.json', 'src/__unknown__'];
235
+ log('Changed files could not be compared because local main was recreated; running safe reconciliation.');
236
+ } else {
237
+ previousPackageJson = readJson('package.json');
238
+ previousLockfile = readJson('package-lock.json') || readJson('npm-shrinkwrap.json');
239
+ changedFiles = await getChangedFiles('HEAD', 'origin/main');
240
+ logChangedFiles(changedFiles);
101
241
  }
102
242
 
103
243
  const isAncestor = await run('git', ['merge-base', '--is-ancestor', 'HEAD', 'origin/main'], {
@@ -118,13 +258,31 @@ async function main() {
118
258
  const packageVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
119
259
  log(`Repository updated to Pixcode ${packageVersion}.`);
120
260
 
121
- await run('npm', ['install', '--no-audit', '--no-fund']);
122
- const updatedPackageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
123
- if (updatedPackageJson.scripts?.build) {
261
+ const updatedPackageJson = readJson('package.json');
262
+ const updatedLockfile = readJson('package-lock.json') || readJson('npm-shrinkwrap.json');
263
+ const installNeeded = shouldRunNpmInstall(
264
+ changedFiles,
265
+ previousPackageJson,
266
+ updatedPackageJson,
267
+ previousLockfile,
268
+ updatedLockfile,
269
+ );
270
+ const buildNeeded = shouldRunBuild(changedFiles, previousPackageJson, updatedPackageJson, installNeeded);
271
+
272
+ if (installNeeded) {
273
+ log('Installing dependencies because package manifests changed.');
274
+ await run('npm', ['install', '--no-audit', '--no-fund']);
275
+ } else {
276
+ log('Dependencies unchanged; skipping npm install.');
277
+ }
278
+
279
+ if (buildNeeded) {
124
280
  log('Building Pixcode source install.');
125
281
  await run('npm', ['run', 'build']);
126
282
  } else {
127
- log('No build script found; skipping build.');
283
+ log(updatedPackageJson?.scripts?.build
284
+ ? 'Build inputs unchanged; skipping build.'
285
+ : 'No build script found; skipping build.');
128
286
  }
129
287
  log('Pixcode git install update completed.');
130
288
  }
@@ -190,7 +190,13 @@ async function spawnGemini(command, options = {}, ws) {
190
190
  args.push('--output-format', 'stream-json');
191
191
 
192
192
  // Handle approval modes and allowed tools
193
- if (settings.skipPermissions || options.skipPermissions || permissionMode === 'yolo') {
193
+ if (
194
+ settings.skipPermissions ||
195
+ options.skipPermissions ||
196
+ permissionMode === 'yolo' ||
197
+ permissionMode === 'bypassPermissions' ||
198
+ permissionMode === 'acceptEdits'
199
+ ) {
194
200
  args.push('--yolo');
195
201
  } else if (permissionMode === 'auto_edit') {
196
202
  args.push('--approval-mode', 'auto_edit');