cortexcode 0.0.0-stage → 0.1.0

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
@@ -1,3 +1,14 @@
1
- # Temporary Holding Version
1
+ # cortexcode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ```sh
4
+ npx cortexcode
5
+ ```
6
+
7
+ Starts [CortexCode](https://github.com/agentbusiness/CortexCode), a local coding agent. The first
8
+ run downloads the app for this machine (Apple Silicon Macs; Linux x64 and arm64) from the official
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.
13
+
14
+ Arguments go to the app: `npx cortexcode --headless`, `npx cortexcode --safe`.
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ // `npx cortexcode`: start CortexCode. The first run downloads the app for this machine from the
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`).
7
+
8
+ import { spawn } from 'node:child_process';
9
+ import { createHash } from 'node:crypto';
10
+ import { closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
11
+ import http from 'node:http';
12
+ import https from 'node:https';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { pipeline } from 'node:stream/promises';
16
+
17
+ const REPO = process.env.CORTEXCODE_REPO || 'agentbusiness/CortexCode';
18
+ // CORTEXCODE_CATALOG: another catalog (a mirror, or a release under test).
19
+ const CATALOG_URL = process.env.CORTEXCODE_CATALOG || `https://github.com/${REPO}/releases/download/catalog/catalog.json`;
20
+ const HOME = process.env.CORTEX_HOME || join(homedir(), '.cortexcode');
21
+ const APPS = join(HOME, 'app');
22
+ const READY = '.ready';
23
+ const START_LOG = join(HOME, 'logs', 'start.log');
24
+ const KEEP = 2;
25
+
26
+ const say = (text) => process.stderr.write(`cortexcode: ${text}\n`);
27
+
28
+ function platform() {
29
+ const key = `${process.platform}-${process.arch}`;
30
+ const known = { 'darwin-arm64': 'darwin-arm64', 'linux-x64': 'linux-x64', 'linux-arm64': 'linux-arm64' };
31
+ if (!known[key]) throw new Error(`CortexCode runs on Apple Silicon Macs and on Linux (x64, arm64), not on ${key}`);
32
+ return known[key];
33
+ }
34
+
35
+ /** `0.3.7` as a comparable triple, or null. */
36
+ function version(text) {
37
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(\+.*)?$/.exec(String(text ?? '').trim());
38
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
39
+ }
40
+
41
+ function compare(a, b) {
42
+ for (let i = 0; i < 3; i += 1) if (a[i] !== b[i]) return a[i] - b[i];
43
+ return 0;
44
+ }
45
+
46
+ /** The response to a GET of `url`, after redirects (GitHub sends release files from another host). */
47
+ function get(url, redirects = 5) {
48
+ return new Promise((resolve, reject) => {
49
+ const client = url.startsWith('https:') ? https : http;
50
+ const request = client.get(url, { headers: { 'user-agent': 'cortexcode-npx' } }, (response) => {
51
+ const status = response.statusCode ?? 0;
52
+ if (status >= 300 && status < 400 && response.headers.location && redirects > 0) {
53
+ response.resume();
54
+ resolve(get(new URL(response.headers.location, url).toString(), redirects - 1));
55
+ return;
56
+ }
57
+ if (status !== 200) {
58
+ response.resume();
59
+ reject(new Error(`${url} answered ${status}`));
60
+ return;
61
+ }
62
+ resolve(response);
63
+ });
64
+ request.on('error', reject);
65
+ });
66
+ }
67
+
68
+ /** The release catalog: the current version of the app and every part, with their files. */
69
+ async function catalog() {
70
+ const response = await fetch(CATALOG_URL, { headers: { 'user-agent': 'cortexcode-npx' } });
71
+ if (!response.ok) throw new Error(`${CATALOG_URL} answered ${response.status}`);
72
+ const parsed = await response.json();
73
+ if (parsed.format !== 'cortexcode-catalog/1') throw new Error(`unknown catalog format ${parsed.format}`);
74
+ return parsed;
75
+ }
76
+
77
+ /** Installed app versions, newest first. */
78
+ function installed() {
79
+ if (!existsSync(APPS)) return [];
80
+ return readdirSync(APPS)
81
+ .filter((name) => version(name) && existsSync(join(APPS, name, READY)))
82
+ .sort((a, b) => compare(version(b), version(a)));
83
+ }
84
+
85
+ function executable(dir) {
86
+ return process.platform === 'darwin' ? join(dir, 'CortexCode.app', 'Contents', 'MacOS', 'CortexCode') : join(dir, 'cortexcode', 'CortexCode');
87
+ }
88
+
89
+ /** Download the app archive, check size and SHA-256, unpack it beside the others. */
90
+ async function install(app, asset) {
91
+ mkdirSync(APPS, { recursive: true });
92
+ const archive = join(APPS, `.download-${app.version}.tar.gz`);
93
+ const staging = join(APPS, `.staging-${app.version}`);
94
+ say(`downloading CortexCode ${app.version} (${Math.round(asset.size / 1e6)} MB)`);
95
+ // node:http rather than fetch: fetch's stream fails when a server closes the connection while the
96
+ // file write is behind.
97
+ const body = await get(asset.url);
98
+ const hash = createHash('sha256');
99
+ let received = 0;
100
+ let reported = 0;
101
+ body.on('data', (chunk) => {
102
+ hash.update(chunk);
103
+ received += chunk.length;
104
+ const percent = asset.size ? Math.floor((received * 100) / asset.size) : 0;
105
+ if (percent >= reported + 10) {
106
+ reported = percent;
107
+ process.stderr.write(`\rcortexcode: ${percent}%`);
108
+ }
109
+ });
110
+ await pipeline(body, createWriteStream(archive));
111
+ process.stderr.write('\n');
112
+ if (received !== asset.size || hash.digest('hex') !== String(asset.sha256).toLowerCase()) {
113
+ rmSync(archive, { force: true });
114
+ throw new Error(`${asset.name} does not match the catalog (size or checksum); it was not used`);
115
+ }
116
+ rmSync(staging, { recursive: true, force: true });
117
+ mkdirSync(staging, { recursive: true });
118
+ await run('tar', ['-xzf', archive, '-C', staging]);
119
+ rmSync(archive, { force: true });
120
+ const target = join(APPS, app.version);
121
+ rmSync(target, { recursive: true, force: true });
122
+ renameSync(staging, target);
123
+ writeFileSync(join(target, READY), '');
124
+ say(`CortexCode ${app.version} is installed in ${target}`);
125
+ }
126
+
127
+ function run(command, args) {
128
+ return new Promise((resolve, reject) => {
129
+ const child = spawn(command, args, { stdio: ['ignore', 'ignore', 'inherit'] });
130
+ child.on('error', reject);
131
+ child.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`${command} failed (${code})`))));
132
+ });
133
+ }
134
+
135
+ /** The pid of a CortexCode running for this home, if any (the frame writes run/frame.json). */
136
+ function running() {
137
+ try {
138
+ const { pid } = JSON.parse(readFileSync(join(HOME, 'run', 'frame.json'), 'utf8'));
139
+ process.kill(pid, 0);
140
+ return pid;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /** Keep the newest versions (and the one starting now). */
147
+ function prune(keep) {
148
+ installed()
149
+ .slice(KEEP)
150
+ .filter((name) => name !== keep)
151
+ .forEach((name) => rmSync(join(APPS, name), { recursive: true, force: true }));
152
+ }
153
+
154
+ async function main() {
155
+ const args = process.argv.slice(2);
156
+ 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');
158
+ return 0;
159
+ }
160
+ const pid = running();
161
+ if (pid) {
162
+ say(`CortexCode is already running (pid ${pid})`);
163
+ return 0;
164
+ }
165
+ 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)) {
176
+ try {
177
+ await install(wanted.app, wanted.asset);
178
+ } catch (error) {
179
+ say(error.message);
180
+ }
181
+ }
182
+ const versions = installed();
183
+ const chosen = wanted && versions.includes(wanted.app.version) ? wanted.app.version : versions[0];
184
+ if (!chosen) {
185
+ say('CortexCode is not installed and could not be downloaded');
186
+ return 1;
187
+ }
188
+ prune(chosen);
189
+ // Its own session, detached from this terminal: closing the terminal does not quit it.
190
+ mkdirSync(join(HOME, 'logs'), { recursive: true });
191
+ const log = openSync(START_LOG, 'a');
192
+ const child = spawn(executable(join(APPS, chosen)), args, { detached: true, stdio: ['ignore', log, log] });
193
+ closeSync(log);
194
+ return await new Promise((resolve) => {
195
+ child.on('error', (error) => {
196
+ say(`cannot start CortexCode: ${error.message}`);
197
+ resolve(1);
198
+ });
199
+ child.on('spawn', () => {
200
+ child.unref();
201
+ say(`CortexCode ${chosen} is starting; you can close this terminal (start output: ${START_LOG})`);
202
+ resolve(0);
203
+ });
204
+ });
205
+ }
206
+
207
+ main().then(
208
+ (code) => process.exit(code),
209
+ (error) => {
210
+ say(error.message);
211
+ process.exit(1);
212
+ },
213
+ );
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "cortexcode",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "CortexCode, a local coding agent: npx cortexcode downloads the app for this machine from the official releases and starts it.",
5
+ "type": "module",
6
+ "bin": {
7
+ "cortexcode": "bin/cortexcode.mjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.17"
15
+ },
16
+ "os": [
17
+ "darwin",
18
+ "linux"
19
+ ],
20
+ "cpu": [
21
+ "arm64",
22
+ "x64"
23
+ ],
24
+ "license": "Apache-2.0",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/agentbusiness/CortexCode.git"
28
+ },
29
+ "homepage": "https://github.com/agentbusiness/CortexCode"
30
+ }