cortexcode 0.1.0 → 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,8 +7,11 @@ 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 it at once and fetch a newer app when one is released. The app runs on its own:
11
- you can close the terminal (its start output goes to `~/.cortexcode/logs/start.log`); running the
12
- command again while it runs only says so. Plugin and frame updates come through the app itself.
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
14
+ runs on its own: you can close the terminal (its start output goes to `~/.cortexcode/logs/start.log`);
15
+ running the command again while it runs only says so.
13
16
 
14
17
  Arguments go to the app: `npx cortexcode --headless`, `npx cortexcode --safe`.
@@ -1,17 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  // `npx cortexcode`: start CortexCode. The first run downloads the app for this machine from the
3
3
  // official releases (the catalog), checks its checksum and unpacks it into ~/.cortexcode/app/<version>/;
4
- // later runs start it at once and fetch a newer app when the catalog names one. The app runs on its
5
- // own: closing the terminal does not quit it (its start output goes to ~/.cortexcode/logs/start.log).
6
- // Plugin and frame updates come through the app itself. Arguments go to the app (`--headless`, `--safe`).
4
+ // later runs start the installed app at once, without looking at the catalog: the app updates its
5
+ // frame and plugins itself, so a newer app release is only for machines that have none. The app runs
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).
8
+ // Arguments go to the app (`--headless`, `--safe`).
7
9
 
8
10
  import { spawn } from 'node:child_process';
9
11
  import { createHash } from 'node:crypto';
10
- 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';
11
13
  import http from 'node:http';
12
14
  import https from 'node:https';
13
15
  import { homedir } from 'node:os';
14
- import { join } from 'node:path';
16
+ import { dirname, join } from 'node:path';
15
17
  import { pipeline } from 'node:stream/promises';
16
18
 
17
19
  const REPO = process.env.CORTEXCODE_REPO || 'agentbusiness/CortexCode';
@@ -143,6 +145,134 @@ function running() {
143
145
  }
144
146
  }
145
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
+
146
276
  /** Keep the newest versions (and the one starting now). */
147
277
  function prune(keep) {
148
278
  installed()
@@ -154,7 +284,7 @@ function prune(keep) {
154
284
  async function main() {
155
285
  const args = process.argv.slice(2);
156
286
  if (args.includes('--help') || args.includes('-h')) {
157
- 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');
158
288
  return 0;
159
289
  }
160
290
  const pid = running();
@@ -163,29 +293,32 @@ async function main() {
163
293
  return 0;
164
294
  }
165
295
  const key = platform();
166
- let wanted = null;
167
- try {
168
- const listed = await catalog();
169
- const files = listed.app?.files?.[key];
170
- if (listed.app && files?.archive) wanted = { app: listed.app, asset: files.archive };
171
- else say(`no CortexCode app is released for ${key} yet`);
172
- } catch (error) {
173
- say(`cannot look for a new version (${error.message})`);
174
- }
175
- if (wanted && !installed().includes(wanted.app.version)) {
296
+ // The installed app updates itself (frame and plugins): only a machine without one downloads.
297
+ if (installed().length === 0) {
176
298
  try {
177
- await install(wanted.app, wanted.asset);
299
+ const listed = await catalog();
300
+ const files = listed.app?.files?.[key];
301
+ if (listed.app && files?.archive) await install(listed.app, files.archive);
302
+ else say(`no CortexCode app is released for ${key} yet`);
178
303
  } catch (error) {
179
- say(error.message);
304
+ say(`cannot download CortexCode (${error.message})`);
180
305
  }
181
306
  }
182
- const versions = installed();
183
- const chosen = wanted && versions.includes(wanted.app.version) ? wanted.app.version : versions[0];
307
+ const chosen = installed()[0];
184
308
  if (!chosen) {
185
309
  say('CortexCode is not installed and could not be downloaded');
186
310
  return 1;
187
311
  }
188
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
+ }
189
322
  // Its own session, detached from this terminal: closing the terminal does not quit it.
190
323
  mkdirSync(join(HOME, 'logs'), { recursive: true });
191
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.0",
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": {