cortexcode 0.1.1 → 0.1.2

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,134 @@ 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
+ /** The starter's script: the newest ready version in APPS, started as this launcher starts it. */
166
+ function starterScript() {
167
+ const start =
168
+ process.platform === 'darwin'
169
+ ? 'exec open "$apps/$version/CortexCode.app" --stdout "$log" --stderr "$log" --args "$@"'
170
+ : 'exec "$apps/$version/cortexcode/CortexCode" "$@" >>"$log" 2>&1';
171
+ const missing =
172
+ process.platform === 'darwin'
173
+ ? `osascript -e 'display alert "CortexCode is not installed" message "Run npx cortexcode in a terminal to install it."' >/dev/null 2>&1`
174
+ : 'command -v notify-send >/dev/null 2>&1 && notify-send CortexCode "CortexCode is not installed: run npx cortexcode in a terminal."';
175
+ return `#!/bin/sh
176
+ # CortexCode starter, written by npx cortexcode: starts the newest app installed in the folder below.
177
+ apps=${shq(APPS)}
178
+ log=${shq(START_LOG)}
179
+ mkdir -p "$(dirname "$log")"
180
+ version="$(ls "$apps" 2>/dev/null | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+$' | sort -t. -k1,1n -k2,2n -k3,3n |
181
+ while read -r v; do [ -e "$apps/$v/${READY}" ] && echo "$v"; done | tail -n 1)"
182
+ if [ -z "$version" ]; then
183
+ ${missing}
184
+ exit 1
185
+ fi
186
+ ${start}
187
+ `;
188
+ }
189
+
190
+ /** macOS: CortexCode.app in /Applications, or in ~/Applications when /Applications is not writable. */
191
+ function macStarter(appDir) {
192
+ const places = ['/Applications', join(homedir(), 'Applications')].map((place) => join(place, 'CortexCode.app'));
193
+ const ours = (bundle) => {
194
+ try {
195
+ return readFileSync(join(bundle, 'Contents', 'Info.plist'), 'utf8').includes(STARTER_ID);
196
+ } catch {
197
+ return false;
198
+ }
199
+ };
200
+ // A CortexCode.app that is not a starter (the disk image's app) is the user's own: leave it.
201
+ if (places.some((bundle) => existsSync(join(bundle, 'Contents', 'Info.plist')) && !ours(bundle))) return;
202
+ let bundle = places.find(ours);
203
+ const fresh = !bundle;
204
+ if (!bundle) {
205
+ for (const place of places) {
206
+ try {
207
+ mkdirSync(join(place, 'Contents', 'MacOS'), { recursive: true });
208
+ bundle = place;
209
+ break;
210
+ } catch {
211
+ // not writable: the next place
212
+ }
213
+ }
214
+ }
215
+ if (!bundle) throw new Error('neither /Applications nor ~/Applications is writable');
216
+ // LSUIElement: the starter only hands over to the app, so it never shows in the Dock itself.
217
+ writeIfChanged(
218
+ join(bundle, 'Contents', 'Info.plist'),
219
+ `<?xml version="1.0" encoding="UTF-8"?>
220
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
221
+ <plist version="1.0">
222
+ <dict>
223
+ <key>CFBundleName</key>
224
+ <string>CortexCode</string>
225
+ <key>CFBundleDisplayName</key>
226
+ <string>CortexCode</string>
227
+ <key>CFBundleIdentifier</key>
228
+ <string>${STARTER_ID}</string>
229
+ <key>CFBundleExecutable</key>
230
+ <string>CortexCode</string>
231
+ <key>CFBundleIconFile</key>
232
+ <string>CortexCode.icns</string>
233
+ <key>CFBundlePackageType</key>
234
+ <string>APPL</string>
235
+ <key>CFBundleShortVersionString</key>
236
+ <string>1.0</string>
237
+ <key>LSUIElement</key>
238
+ <true/>
239
+ </dict>
240
+ </plist>
241
+ `,
242
+ );
243
+ writeIfChanged(join(bundle, 'Contents', 'MacOS', 'CortexCode'), starterScript(), 0o755);
244
+ const icon = join(bundle, 'Contents', 'Resources', 'CortexCode.icns');
245
+ if (!existsSync(icon)) {
246
+ mkdirSync(dirname(icon), { recursive: true });
247
+ copyFileSync(join(appDir, 'CortexCode.app', 'Contents', 'Resources', 'CortexCode.icns'), icon);
248
+ }
249
+ if (fresh) say(`CortexCode is in ${dirname(bundle)}: open it from there next time`);
250
+ }
251
+
252
+ /** Linux: a desktop entry in the app list ($XDG_DATA_HOME/applications) that runs APPS/start. */
253
+ function linuxStarter(appDir) {
254
+ const script = join(APPS, 'start');
255
+ const icon = join(APPS, 'cortexcode.png');
256
+ writeIfChanged(script, starterScript(), 0o755);
257
+ if (!existsSync(icon)) copyFileSync(join(appDir, 'cortexcode', 'ui', 'shell', 'assets', 'cortexcode-512.png'), icon);
258
+ const data = process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
259
+ const entry = join(data, 'applications', 'cortexcode.desktop');
260
+ const fresh = !existsSync(entry);
261
+ writeIfChanged(
262
+ entry,
263
+ `[Desktop Entry]
264
+ Type=Application
265
+ Name=CortexCode
266
+ Comment=Local coding agent
267
+ Exec="${script}"
268
+ Icon=${icon}
269
+ Terminal=false
270
+ Categories=Development;
271
+ `,
272
+ );
273
+ if (fresh) say('CortexCode is in your applications: open it from there next time');
274
+ }
275
+
147
276
  /** Keep the newest versions (and the one starting now). */
148
277
  function prune(keep) {
149
278
  installed()
@@ -155,7 +284,7 @@ function prune(keep) {
155
284
  async function main() {
156
285
  const args = process.argv.slice(2);
157
286
  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');
287
+ 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
288
  return 0;
160
289
  }
161
290
  const pid = running();
@@ -181,6 +310,15 @@ async function main() {
181
310
  return 1;
182
311
  }
183
312
  prune(chosen);
313
+ // Only for the user's own home: a scratch home (CORTEX_HOME) must not take over the starter.
314
+ if (!process.env.CORTEX_HOME) {
315
+ try {
316
+ if (process.platform === 'darwin') macStarter(join(APPS, chosen));
317
+ else linuxStarter(join(APPS, chosen));
318
+ } catch (error) {
319
+ say(`cannot add CortexCode to the applications (${error.message})`);
320
+ }
321
+ }
184
322
  // Its own session, detached from this terminal: closing the terminal does not quit it.
185
323
  mkdirSync(join(HOME, 'logs'), { recursive: true });
186
324
  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.2",
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": {