@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.
- package/dist/assets/{index-Dw9ELh9s.js → index-B3sFoGyf.js} +161 -161
- package/dist/assets/index-BtdtY_p1.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/gemini-cli.js +5 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +189 -17
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js +87 -0
- package/dist-server/server/modules/orchestration/hermes/hermes.routes.js.map +1 -1
- package/dist-server/server/qwen-code-cli.js +5 -1
- package/dist-server/server/qwen-code-cli.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hermes/configure-pixcode-mcp.mjs +87 -0
- package/scripts/hermes/pixcode-mcp-server.mjs +216 -0
- package/scripts/smoke/git-install-update.mjs +133 -54
- package/scripts/smoke/pixcode-workbench-1-48.mjs +36 -1
- package/scripts/smoke/vscode-workbench-polish.mjs +21 -2
- package/scripts/update-git-install.mjs +162 -4
- package/server/gemini-cli.js +7 -1
- package/server/index.js +207 -17
- package/server/modules/orchestration/hermes/hermes.routes.ts +119 -0
- package/server/qwen-code-cli.js +7 -1
- package/dist/assets/index-B3lN7dBd.css +0 -32
|
@@ -65,20 +65,51 @@ assert.match(
|
|
|
65
65
|
|
|
66
66
|
assert.match(
|
|
67
67
|
updater,
|
|
68
|
-
/
|
|
69
|
-
'Safe updater should
|
|
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
|
|
75
|
-
'Safe updater should
|
|
74
|
+
/Dependencies unchanged; skipping npm install\./,
|
|
75
|
+
'Safe updater should skip npm install when package manifests did not change.',
|
|
76
76
|
);
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fs.writeFileSync(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
run('git', ['
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
run(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
fs.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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, /
|
|
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
|
-
/
|
|
256
|
-
'
|
|
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
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
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(
|
|
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
|
}
|
package/server/gemini-cli.js
CHANGED
|
@@ -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 (
|
|
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');
|