cortexcode 0.1.1 → 0.1.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.
package/README.md CHANGED
@@ -7,7 +7,10 @@ npx cortexcode
7
7
  Starts [CortexCode](https://github.com/agentbusiness/CortexCode), a local coding agent. The first
8
8
  run downloads the app for this machine (Apple Silicon Macs; Linux x64 and arm64) from the official
9
9
  releases, checks it against the checksum in the release catalog and unpacks it into `~/.cortexcode/app/`.
10
- Later runs start the installed app at once; the app updates its frame and plugins itself. The app
10
+ Later runs start the installed app at once; the app updates its frame and plugins itself. It also
11
+ adds CortexCode to your applications (macOS: `/Applications`, or `~/Applications` when that is not
12
+ writable; Linux: `~/.local/share/applications`), which starts the newest installed version, so you
13
+ can open it without a terminal. The app
11
14
  runs on its own: you can close the terminal (its start output goes to `~/.cortexcode/logs/start.log`);
12
15
  running the command again while it runs only says so.
13
16
 
@@ -4,15 +4,16 @@
4
4
  // later runs start the installed app at once, without looking at the catalog: the app updates its
5
5
  // frame and plugins itself, so a newer app release is only for machines that have none. The app runs
6
6
  // on its own: closing the terminal does not quit it (its start output goes to ~/.cortexcode/logs/start.log).
7
+ // Every start also keeps a starter in the Applications folder (macOS) or the app list (Linux).
7
8
  // Arguments go to the app (`--headless`, `--safe`).
8
9
 
9
10
  import { spawn } from 'node:child_process';
10
11
  import { createHash } from 'node:crypto';
11
- import { closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
12
13
  import http from 'node:http';
13
14
  import https from 'node:https';
14
15
  import { homedir } from 'node:os';
15
- import { join } from 'node:path';
16
+ import { dirname, join } from 'node:path';
16
17
  import { pipeline } from 'node:stream/promises';
17
18
 
18
19
  const REPO = process.env.CORTEXCODE_REPO || 'agentbusiness/CortexCode';
@@ -144,6 +145,148 @@ function running() {
144
145
  }
145
146
  }
146
147
 
148
+ // The starter: a small app in the Applications folder (macOS) or the desktop's app list (Linux) that
149
+ // starts the newest installed version, so the app opens without a terminal. It copies nothing of the
150
+ // app, so it keeps working when updates add versions and prune old ones.
151
+ const STARTER_ID = 'ai.cortexcode.starter';
152
+
153
+ /** `text` quoted for sh. */
154
+ const shq = (text) => `'${String(text).replace(/'/g, `'\\''`)}'`;
155
+
156
+ /** Write `text` to `path` unless it already holds it; true when it wrote. */
157
+ function writeIfChanged(path, text, mode = 0o644) {
158
+ mkdirSync(dirname(path), { recursive: true });
159
+ if (existsSync(path) && readFileSync(path, 'utf8') === text) return false;
160
+ writeFileSync(path, text);
161
+ chmodSync(path, mode);
162
+ return true;
163
+ }
164
+
165
+ /**
166
+ * The starter's script: the newest ready version in APPS, started exactly as this launcher starts it
167
+ * (its binary run directly, detached, output to the start log). Not through `open`: that hands the
168
+ * bundle to LaunchServices, which starts it differently from a terminal. When the app already runs,
169
+ * it brings the app's window forward instead (macOS).
170
+ */
171
+ function starterScript() {
172
+ const exe = process.platform === 'darwin' ? '$apps/$version/CortexCode.app/Contents/MacOS/CortexCode' : '$apps/$version/cortexcode/CortexCode';
173
+ // The window belongs to the frame's Electron child: opening that running bundle activates it.
174
+ const forward =
175
+ process.platform === 'darwin'
176
+ ? ` shell="$(ps -axo ppid=,comm= | awk -v p="$pid" '$1 == p && /\\.app\\/Contents\\/MacOS\\// { sub(/^ *[0-9]+ +/, ""); sub(/\\/Contents\\/MacOS\\/.*/, ""); print; exit }')"
177
+ [ -n "$shell" ] && open "$shell"
178
+ `
179
+ : '';
180
+ const missing =
181
+ process.platform === 'darwin'
182
+ ? `osascript -e 'display alert "CortexCode is not installed" message "Run npx cortexcode in a terminal to install it."' >/dev/null 2>&1`
183
+ : 'command -v notify-send >/dev/null 2>&1 && notify-send CortexCode "CortexCode is not installed: run npx cortexcode in a terminal."';
184
+ return `#!/bin/sh
185
+ # CortexCode starter, written by npx cortexcode: starts the newest app installed in the folder below.
186
+ apps=${shq(APPS)}
187
+ log=${shq(START_LOG)}
188
+ pid="$(sed -n 's/.*"pid": *\\([0-9][0-9]*\\).*/\\1/p' ${shq(join(HOME, 'run', 'frame.json'))} 2>/dev/null)"
189
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
190
+ ${forward} exit 0
191
+ fi
192
+ mkdir -p "$(dirname "$log")"
193
+ version="$(ls "$apps" 2>/dev/null | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+$' | sort -t. -k1,1n -k2,2n -k3,3n |
194
+ while read -r v; do [ -e "$apps/$v/${READY}" ] && echo "$v"; done | tail -n 1)"
195
+ if [ -z "$version" ]; then
196
+ ${missing}
197
+ exit 1
198
+ fi
199
+ # Detached, so the starter ends at once and opening it again finds the running app.
200
+ nohup "${exe}" "$@" >>"$log" 2>&1 </dev/null &
201
+ `;
202
+ }
203
+
204
+ /** macOS: CortexCode.app in /Applications, or in ~/Applications when /Applications is not writable. */
205
+ function macStarter(appDir) {
206
+ const places = ['/Applications', join(homedir(), 'Applications')].map((place) => join(place, 'CortexCode.app'));
207
+ const ours = (bundle) => {
208
+ try {
209
+ return readFileSync(join(bundle, 'Contents', 'Info.plist'), 'utf8').includes(STARTER_ID);
210
+ } catch {
211
+ return false;
212
+ }
213
+ };
214
+ // A CortexCode.app that is not a starter (the disk image's app) is the user's own: leave it.
215
+ if (places.some((bundle) => existsSync(join(bundle, 'Contents', 'Info.plist')) && !ours(bundle))) return;
216
+ let bundle = places.find(ours);
217
+ const fresh = !bundle;
218
+ if (!bundle) {
219
+ for (const place of places) {
220
+ try {
221
+ mkdirSync(join(place, 'Contents', 'MacOS'), { recursive: true });
222
+ bundle = place;
223
+ break;
224
+ } catch {
225
+ // not writable: the next place
226
+ }
227
+ }
228
+ }
229
+ if (!bundle) throw new Error('neither /Applications nor ~/Applications is writable');
230
+ // LSUIElement: the starter only hands over to the app, so it never shows in the Dock itself.
231
+ writeIfChanged(
232
+ join(bundle, 'Contents', 'Info.plist'),
233
+ `<?xml version="1.0" encoding="UTF-8"?>
234
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
235
+ <plist version="1.0">
236
+ <dict>
237
+ <key>CFBundleName</key>
238
+ <string>CortexCode</string>
239
+ <key>CFBundleDisplayName</key>
240
+ <string>CortexCode</string>
241
+ <key>CFBundleIdentifier</key>
242
+ <string>${STARTER_ID}</string>
243
+ <key>CFBundleExecutable</key>
244
+ <string>CortexCode</string>
245
+ <key>CFBundleIconFile</key>
246
+ <string>CortexCode.icns</string>
247
+ <key>CFBundlePackageType</key>
248
+ <string>APPL</string>
249
+ <key>CFBundleShortVersionString</key>
250
+ <string>1.0</string>
251
+ <key>LSUIElement</key>
252
+ <true/>
253
+ </dict>
254
+ </plist>
255
+ `,
256
+ );
257
+ writeIfChanged(join(bundle, 'Contents', 'MacOS', 'CortexCode'), starterScript(), 0o755);
258
+ const icon = join(bundle, 'Contents', 'Resources', 'CortexCode.icns');
259
+ if (!existsSync(icon)) {
260
+ mkdirSync(dirname(icon), { recursive: true });
261
+ copyFileSync(join(appDir, 'CortexCode.app', 'Contents', 'Resources', 'CortexCode.icns'), icon);
262
+ }
263
+ if (fresh) say(`CortexCode is in ${dirname(bundle)}: open it from there next time`);
264
+ }
265
+
266
+ /** Linux: a desktop entry in the app list ($XDG_DATA_HOME/applications) that runs APPS/start. */
267
+ function linuxStarter(appDir) {
268
+ const script = join(APPS, 'start');
269
+ const icon = join(APPS, 'cortexcode.png');
270
+ writeIfChanged(script, starterScript(), 0o755);
271
+ if (!existsSync(icon)) copyFileSync(join(appDir, 'cortexcode', 'ui', 'shell', 'assets', 'cortexcode-512.png'), icon);
272
+ const data = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
273
+ const entry = join(data, 'applications', 'cortexcode.desktop');
274
+ const fresh = !existsSync(entry);
275
+ writeIfChanged(
276
+ entry,
277
+ `[Desktop Entry]
278
+ Type=Application
279
+ Name=CortexCode
280
+ Comment=Local coding agent
281
+ Exec="${script}"
282
+ Icon=${icon}
283
+ Terminal=false
284
+ Categories=Development;
285
+ `,
286
+ );
287
+ if (fresh) say('CortexCode is in your applications: open it from there next time');
288
+ }
289
+
147
290
  /** Keep the newest versions (and the one starting now). */
148
291
  function prune(keep) {
149
292
  installed()
@@ -155,7 +298,7 @@ function prune(keep) {
155
298
  async function main() {
156
299
  const args = process.argv.slice(2);
157
300
  if (args.includes('--help') || args.includes('-h')) {
158
- process.stdout.write('npx cortexcode [--headless] [--safe]\n\nStarts CortexCode; the first run downloads it into ~/.cortexcode/app.\n');
301
+ process.stdout.write('npx cortexcode [--headless] [--safe]\n\nStarts CortexCode; the first run downloads it into ~/.cortexcode/app and adds it to your applications.\n');
159
302
  return 0;
160
303
  }
161
304
  const pid = running();
@@ -181,6 +324,15 @@ async function main() {
181
324
  return 1;
182
325
  }
183
326
  prune(chosen);
327
+ // Only for the user's own home: a scratch home (CORTEX_HOME) must not take over the starter.
328
+ if (!process.env.CORTEX_HOME) {
329
+ try {
330
+ if (process.platform === 'darwin') macStarter(join(APPS, chosen));
331
+ else linuxStarter(join(APPS, chosen));
332
+ } catch (error) {
333
+ say(`cannot add CortexCode to the applications (${error.message})`);
334
+ }
335
+ }
184
336
  // Its own session, detached from this terminal: closing the terminal does not quit it.
185
337
  mkdirSync(join(HOME, 'logs'), { recursive: true });
186
338
  const log = openSync(START_LOG, 'a');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cortexcode",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CortexCode, a local coding agent: npx cortexcode downloads the app for this machine from the official releases and starts it.",
5
5
  "type": "module",
6
6
  "bin": {