@pixelbyte-software/pixcode 1.47.1 → 1.47.3

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,176 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+
7
+ const serverIndex = fs.readFileSync('server/index.js', 'utf8');
8
+ const modal = fs.readFileSync('src/components/version-upgrade/view/VersionUpgradeModal.tsx', 'utf8');
9
+ const updater = fs.readFileSync('scripts/update-git-install.mjs', 'utf8');
10
+ const updaterPath = path.resolve('scripts/update-git-install.mjs');
11
+
12
+ assert.match(
13
+ serverIndex,
14
+ /update-git-install\.mjs/,
15
+ 'Git install updates should use the safe updater script instead of raw git pull.',
16
+ );
17
+
18
+ assert.doesNotMatch(
19
+ serverIndex,
20
+ /git checkout main && git pull && npm install/,
21
+ 'Server update command should not use the brittle raw git checkout/pull/install chain.',
22
+ );
23
+
24
+ assert.match(
25
+ serverIndex,
26
+ /updateCommandLabel[\s\S]*Pixcode source update/,
27
+ 'Server update stream should describe git installs with product language instead of an internal script command.',
28
+ );
29
+
30
+ assert.match(
31
+ modal,
32
+ /versionUpdate\.pixcodeUpgradeCommand/,
33
+ 'Version modal should show the user-facing Pixcode update command.',
34
+ );
35
+
36
+ assert.doesNotMatch(
37
+ modal,
38
+ /node scripts\/update-git-install\.mjs/,
39
+ 'Version modal should not expose the internal git updater script as manual product guidance.',
40
+ );
41
+
42
+ assert.match(
43
+ fs.readFileSync('server/cli.js', 'utf8'),
44
+ /update-git-install\.mjs[\s\S]*installMode === 'git'[\s\S]*updateGitPackage/,
45
+ 'pixcode update should drive the safe git updater for source installs.',
46
+ );
47
+
48
+ assert.match(
49
+ updater,
50
+ /stash[\s\S]*push[\s\S]*--include-untracked[\s\S]*--message/,
51
+ 'Safe updater should stash dirty tracked and untracked files before updating.',
52
+ );
53
+
54
+ assert.match(
55
+ updater,
56
+ /branch[\s\S]*backupBranch/,
57
+ 'Safe updater should preserve divergent local commits in a backup branch.',
58
+ );
59
+
60
+ assert.match(
61
+ updater,
62
+ /reset[\s\S]*--hard[\s\S]*origin\/main/,
63
+ 'Safe updater should be able to normalize a divergent install checkout after preserving it.',
64
+ );
65
+
66
+ assert.match(
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.',
70
+ );
71
+
72
+ assert.match(
73
+ updater,
74
+ /npm[\s\S]*run[\s\S]*build/,
75
+ 'Safe updater should rebuild source installs after updating source files.',
76
+ );
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');
82
+
83
+ function run(command, args, cwd) {
84
+ const result = spawnSync(command, args, {
85
+ cwd,
86
+ encoding: 'utf8',
87
+ env: {
88
+ ...process.env,
89
+ GIT_AUTHOR_NAME: 'Pixcode Smoke',
90
+ GIT_AUTHOR_EMAIL: 'smoke@pixcode.local',
91
+ GIT_COMMITTER_NAME: 'Pixcode Smoke',
92
+ GIT_COMMITTER_EMAIL: 'smoke@pixcode.local',
93
+ },
94
+ });
95
+
96
+ assert.equal(
97
+ result.status,
98
+ 0,
99
+ `${command} ${args.join(' ')} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
100
+ );
101
+
102
+ return result.stdout.trim();
103
+ }
104
+
105
+ function writePackage(version) {
106
+ fs.writeFileSync(
107
+ path.join(source, 'package.json'),
108
+ JSON.stringify({
109
+ name: 'pixcode-update-smoke',
110
+ version,
111
+ scripts: {
112
+ build: 'node -e "require(\\"node:fs\\").writeFileSync(\\"built.txt\\", \\"built\\")"',
113
+ },
114
+ }, null, 2),
115
+ );
116
+ fs.writeFileSync(
117
+ path.join(source, 'package-lock.json'),
118
+ JSON.stringify({
119
+ name: 'pixcode-update-smoke',
120
+ version,
121
+ lockfileVersion: 3,
122
+ requires: true,
123
+ packages: {
124
+ '': {
125
+ name: 'pixcode-update-smoke',
126
+ version,
127
+ },
128
+ },
129
+ }, null, 2),
130
+ );
131
+ }
132
+
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
+ );
175
+
176
+ console.log('git install update smoke passed');
@@ -80,6 +80,12 @@ assert.match(
80
80
  'Workbench should request compact composer behavior in the right CLI pane.',
81
81
  );
82
82
 
83
+ assert.match(
84
+ workbench,
85
+ /activeTab === 'chat' && activityPanel === 'projects'/,
86
+ 'Projects activity should stay selected while the center chat tab is active.',
87
+ );
88
+
83
89
  assert.match(chatInterface, /compactComposer\?: boolean/, 'ChatInterface should expose compactComposer for narrow workbench panes.');
84
90
  assert.match(chatComposer, /compact\?: boolean/, 'ChatComposer should expose a compact prop.');
85
91
  assert.match(chatComposer, /flex-wrap/, 'ChatComposer footer should wrap controls in narrow panes.');
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ const repoRoot = process.cwd();
7
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
8
+ const stashMessage = `pixcode-auto-update-${timestamp}`;
9
+ const backupBranch = `pixcode-backup-before-update-${timestamp}`;
10
+
11
+ function log(message) {
12
+ process.stdout.write(`${message}\n`);
13
+ }
14
+
15
+ function run(command, args, options = {}) {
16
+ const {
17
+ allowFailure = false,
18
+ collectOutput = false,
19
+ env = process.env,
20
+ } = options;
21
+
22
+ return new Promise((resolve, reject) => {
23
+ log(`$ ${[command, ...args].join(' ')}`);
24
+ const child = spawn(command, args, {
25
+ cwd: repoRoot,
26
+ env,
27
+ shell: false,
28
+ stdio: ['ignore', 'pipe', 'pipe'],
29
+ });
30
+
31
+ let stdout = '';
32
+ let stderr = '';
33
+
34
+ child.stdout?.on('data', (chunk) => {
35
+ const text = chunk.toString();
36
+ stdout += text;
37
+ process.stdout.write(text);
38
+ });
39
+
40
+ child.stderr?.on('data', (chunk) => {
41
+ const text = chunk.toString();
42
+ stderr += text;
43
+ process.stderr.write(text);
44
+ });
45
+
46
+ child.on('error', reject);
47
+ child.on('close', (code) => {
48
+ const result = { code, stdout, stderr };
49
+ if (code === 0 || allowFailure) {
50
+ resolve(collectOutput ? result : code);
51
+ return;
52
+ }
53
+
54
+ const error = new Error(`${command} ${args.join(' ')} exited with code ${code}`);
55
+ error.result = result;
56
+ reject(error);
57
+ });
58
+ });
59
+ }
60
+
61
+ async function getOutput(command, args, options = {}) {
62
+ const result = await run(command, args, { ...options, collectOutput: true });
63
+ return result.stdout.trim();
64
+ }
65
+
66
+ async function main() {
67
+ if (!fs.existsSync(path.join(repoRoot, '.git'))) {
68
+ throw new Error(`Git metadata not found in ${repoRoot}`);
69
+ }
70
+
71
+ log('Pixcode safe git update started.');
72
+ log(`Repository: ${repoRoot}`);
73
+
74
+ await run('git', ['rev-parse', '--is-inside-work-tree']);
75
+ await run('git', ['fetch', 'origin', 'main']);
76
+
77
+ const status = await getOutput('git', ['status', '--porcelain', '--untracked-files=all']);
78
+ if (status) {
79
+ log('Local checkout has modified or untracked files.');
80
+ log(`Saving them to git stash: ${stashMessage}`);
81
+ await run('git', [
82
+ '-c',
83
+ 'user.name=Pixcode Updater',
84
+ '-c',
85
+ 'user.email=updater@pixcode.local',
86
+ 'stash',
87
+ 'push',
88
+ '--include-untracked',
89
+ '--message',
90
+ stashMessage,
91
+ ]);
92
+ log('Local changes are preserved in git stash.');
93
+ } else {
94
+ log('Working tree is clean.');
95
+ }
96
+
97
+ const checkoutMain = await run('git', ['checkout', 'main'], { allowFailure: true, collectOutput: true });
98
+ if (checkoutMain.code !== 0) {
99
+ log('Local main branch checkout failed; recreating main from origin/main.');
100
+ await run('git', ['checkout', '-B', 'main', 'origin/main']);
101
+ }
102
+
103
+ const isAncestor = await run('git', ['merge-base', '--is-ancestor', 'HEAD', 'origin/main'], {
104
+ allowFailure: true,
105
+ collectOutput: true,
106
+ });
107
+
108
+ if (isAncestor.code === 0) {
109
+ await run('git', ['merge', '--ff-only', 'origin/main']);
110
+ } else if (isAncestor.code === 1) {
111
+ log(`Local main has commits that are not on origin/main. Preserving them in branch: ${backupBranch}`);
112
+ await run('git', ['branch', backupBranch]);
113
+ await run('git', ['reset', '--hard', 'origin/main']);
114
+ } else {
115
+ throw new Error('Could not compare local main with origin/main.');
116
+ }
117
+
118
+ const packageVersion = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')).version;
119
+ log(`Repository updated to Pixcode ${packageVersion}.`);
120
+
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) {
124
+ log('Building Pixcode source install.');
125
+ await run('npm', ['run', 'build']);
126
+ } else {
127
+ log('No build script found; skipping build.');
128
+ }
129
+ log('Pixcode git install update completed.');
130
+ }
131
+
132
+ main().catch((error) => {
133
+ process.stderr.write(`Pixcode git install update failed: ${error.message}\n`);
134
+ process.exit(1);
135
+ });
package/server/cli.js CHANGED
@@ -249,10 +249,77 @@ async function checkForUpdates(silent = false) {
249
249
  }
250
250
  }
251
251
 
252
+ function runInherited(command, args, options = {}) {
253
+ return new Promise((resolve, reject) => {
254
+ const child = spawn(command, args, {
255
+ cwd: options.cwd || APP_ROOT,
256
+ env: options.env || process.env,
257
+ stdio: 'inherit',
258
+ shell: false,
259
+ windowsHide: false,
260
+ });
261
+
262
+ child.on('error', reject);
263
+ child.on('close', (code) => {
264
+ if (code === 0) {
265
+ resolve();
266
+ return;
267
+ }
268
+
269
+ reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
270
+ });
271
+ });
272
+ }
273
+
274
+ async function maybeRestartDaemonAfterUpdate(options = {}) {
275
+ if (options.restartDaemon) {
276
+ if (!hasInstalledDaemonUnit()) {
277
+ console.log(`${c.warn('[WARN]')} No daemon unit detected; skipping restart.`);
278
+ return;
279
+ }
280
+ console.log(`${c.info('[INFO]')} Restarting daemon service...`);
281
+ await handleDaemonCommand(['restart', '--mode=system'], {
282
+ appRoot: APP_ROOT,
283
+ defaultPort: process.env.SERVER_PORT || process.env.PORT || '3001',
284
+ color: c,
285
+ });
286
+ console.log(`${c.ok('[OK]')} Daemon restart completed.`);
287
+ return;
288
+ }
289
+
290
+ if (hasInstalledDaemonUnit()) {
291
+ const restartCommand = buildDaemonCliCommand(
292
+ { subcommand: 'restart', mode: 'system' },
293
+ DAEMON_COMMAND_CONTEXT
294
+ );
295
+ console.log(`${c.tip('[TIP]')} Daemon unit detected. Restart to apply update: ${c.bright(restartCommand)}`);
296
+ console.log(`${c.tip('[TIP]')} Or update + restart in one step: ${c.bright('pixcode update --restart-daemon')}`);
297
+ } else {
298
+ console.log(`${c.tip('[TIP]')} Restart pixcode to use the new version.`);
299
+ }
300
+ }
301
+
302
+ async function updateGitPackage(options = {}) {
303
+ const gitUpdateScript = path.join(APP_ROOT, 'scripts', 'update-git-install.mjs');
304
+ if (!fs.existsSync(gitUpdateScript)) {
305
+ throw new Error(`Git update script was not found: ${gitUpdateScript}`);
306
+ }
307
+
308
+ console.log(`${c.info('[INFO]')} Updating Pixcode source checkout...`);
309
+ await runInherited(process.execPath, [gitUpdateScript], { cwd: APP_ROOT });
310
+ console.log(`${c.ok('[OK]')} Source update complete!`);
311
+ await maybeRestartDaemonAfterUpdate(options);
312
+ }
313
+
252
314
  // Update the package
253
315
  async function updatePackage(options = {}) {
254
316
  try {
255
317
  const { execSync } = await import('child_process');
318
+ if (installMode === 'git') {
319
+ await updateGitPackage(options);
320
+ return;
321
+ }
322
+
256
323
  console.log(`${c.info('[INFO]')} Checking for updates...`);
257
324
 
258
325
  const { hasUpdate, latestVersion, currentVersion } = await checkForUpdates(true);
@@ -265,32 +332,13 @@ async function updatePackage(options = {}) {
265
332
  console.log(`${c.info('[INFO]')} Updating from ${currentVersion} to ${latestVersion}...`);
266
333
  execSync('npm update -g @pixelbyte-software/pixcode', { stdio: 'inherit' });
267
334
  console.log(`${c.ok('[OK]')} Update complete!`);
268
-
269
- if (options.restartDaemon) {
270
- if (!hasInstalledDaemonUnit()) {
271
- console.log(`${c.warn('[WARN]')} No daemon unit detected; skipping restart.`);
272
- return;
273
- }
274
- console.log(`${c.info('[INFO]')} Restarting daemon service...`);
275
- await handleDaemonCommand(['restart', '--mode=system'], {
276
- appRoot: APP_ROOT,
277
- defaultPort: process.env.SERVER_PORT || process.env.PORT || '3001',
278
- color: c,
279
- });
280
- console.log(`${c.ok('[OK]')} Daemon restart completed.`);
281
- } else if (hasInstalledDaemonUnit()) {
282
- const restartCommand = buildDaemonCliCommand(
283
- { subcommand: 'restart', mode: 'system' },
284
- DAEMON_COMMAND_CONTEXT
285
- );
286
- console.log(`${c.tip('[TIP]')} Daemon unit detected. Restart to apply update: ${c.bright(restartCommand)}`);
287
- console.log(`${c.tip('[TIP]')} Or update + restart in one step: ${c.bright('pixcode update --restart-daemon')}`);
288
- } else {
289
- console.log(`${c.tip('[TIP]')} Restart pixcode to use the new version.`);
290
- }
335
+ await maybeRestartDaemonAfterUpdate(options);
291
336
  } catch (e) {
292
337
  console.error(`${c.error('[ERROR]')} Update failed: ${e.message}`);
293
- console.log(`${c.tip('[TIP]')} Try running manually: npm update -g @pixelbyte-software/pixcode`);
338
+ const fallbackCommand = installMode === 'git'
339
+ ? 'pixcode update --restart-daemon'
340
+ : 'npm update -g @pixelbyte-software/pixcode';
341
+ console.log(`${c.tip('[TIP]')} Try running manually: ${fallbackCommand}`);
294
342
  }
295
343
  }
296
344
 
package/server/index.js CHANGED
@@ -482,19 +482,27 @@ app.use(express.static(path.join(APP_ROOT, 'public'), {
482
482
  // npm tarball, extracts it to the writable runtime dir, and triggers
483
483
  // a server restart so the Electron wrapper respawns with new code.
484
484
  // ~4 MB download, ~10 s; no npm/git/shell required on the host.
485
- // 2. installMode === 'git' → `git pull && npm install` in-place.
485
+ // 2. installMode === 'git' → safe git updater script. It stashes dirty
486
+ // checkout state before pulling so source installs do not fail on local
487
+ // modified files left by older releases or manual edits.
486
488
  // 3. fallback → `npm install -g …` (classic npm-distributed install).
487
489
  app.post('/api/system/update', authenticateToken, async (req, res) => {
488
490
  const projectRoot = APP_ROOT;
489
491
  console.log('Starting system update from directory:', projectRoot);
490
492
 
491
493
  const runtimeDir = process.env.PIXCODE_RUNTIME_DIR || null;
494
+ const gitUpdateScript = path.join(projectRoot, 'scripts', 'update-git-install.mjs');
492
495
 
493
496
  const updateCommand = IS_PLATFORM
494
497
  ? 'npm run update:platform'
495
498
  : installMode === 'git'
496
- ? 'git checkout main && git pull && npm install'
499
+ ? `${JSON.stringify(process.execPath)} ${JSON.stringify(gitUpdateScript)}`
497
500
  : 'npm install -g @pixelbyte-software/pixcode@latest';
501
+ const updateCommandLabel = IS_PLATFORM
502
+ ? 'Pixcode platform update'
503
+ : installMode === 'git'
504
+ ? 'Pixcode source update'
505
+ : 'pixcode update';
498
506
 
499
507
  const updateCwd = IS_PLATFORM || installMode === 'git'
500
508
  ? projectRoot
@@ -738,8 +746,9 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
738
746
  // Short-circuit for "already on latest" in the npm-global path so
739
747
  // users don't accidentally crash their own daemon by clicking Update
740
748
  // while already up to date. The runtime-dir branch above already has
741
- // this guard (line ~504); replicate it for npm mode. Git mode skips
742
- // this since `git pull` is harmless when already up to date.
749
+ // this guard (line ~504); replicate it for npm mode. Git mode still
750
+ // runs because users may be on the latest package version but behind
751
+ // the source branch or have a dirty checkout that needs normalization.
743
752
  if (!IS_PLATFORM && installMode === 'npm') {
744
753
  try {
745
754
  send('log', { stream: 'meta', chunk: 'Querying registry for latest version…\n' });
@@ -766,7 +775,7 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
766
775
  }
767
776
  }
768
777
 
769
- send('log', { stream: 'meta', chunk: `Running: ${updateCommand}\n` });
778
+ send('log', { stream: 'meta', chunk: `Running: ${updateCommandLabel}\n` });
770
779
 
771
780
  // Cross-platform shell invocation. `detached: true` + `unref()` below
772
781
  // means the install child survives if this server process gets killed