termify-agent 1.0.61 → 1.0.62
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/package.json +1 -1
- package/scripts/postinstall.js +109 -0
- package/scripts/postinstall.test.mjs +63 -2
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* `termify-agent start` for faster npm install times. See src/setup.ts.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import Conf from 'conf';
|
|
17
18
|
import { execSync } from 'child_process';
|
|
18
19
|
import { createWriteStream, mkdirSync, chmodSync, existsSync, unlinkSync, copyFileSync, rmSync, readFileSync, readlinkSync, writeFileSync, symlinkSync, lstatSync, readdirSync } from 'fs';
|
|
19
20
|
import { isAbsolute, join, dirname, resolve } from 'path';
|
|
@@ -33,6 +34,7 @@ const TERMIFY_DIR = join(homedir(), '.termify');
|
|
|
33
34
|
const DAEMON_PATH = join(TERMIFY_DIR, `termify-daemon${EXE}`);
|
|
34
35
|
const STATS_AGENT_PATH = join(TERMIFY_DIR, `stats-agent${EXE}`);
|
|
35
36
|
const WG_GO_PATH = join(TERMIFY_DIR, 'wireguard-go');
|
|
37
|
+
const DISTILL_MARKER = '## Distill';
|
|
36
38
|
|
|
37
39
|
// Marker: skip node-pty test on repeated installs if it already works for this ABI
|
|
38
40
|
const NODE_PTY_OK_MARKER = join(TERMIFY_DIR, `.node-pty-ok-${process.versions.modules}`);
|
|
@@ -520,6 +522,101 @@ async function setupDistill() {
|
|
|
520
522
|
return { installed: true, degraded: !reachable, shimPath: distillShimPath };
|
|
521
523
|
}
|
|
522
524
|
|
|
525
|
+
function readConfiguredServerUrl() {
|
|
526
|
+
try {
|
|
527
|
+
const config = new Conf({
|
|
528
|
+
projectName: 'termify-agent',
|
|
529
|
+
defaults: {
|
|
530
|
+
serverUrl: 'https://termify.justdiego.com',
|
|
531
|
+
servers: [],
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
const servers = config.get('servers') || [];
|
|
536
|
+
if (Array.isArray(servers) && servers.length > 0) {
|
|
537
|
+
const primary = servers.find((server) => server && server.isPrimary) || servers[0];
|
|
538
|
+
if (primary?.serverUrl) return primary.serverUrl;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const serverUrl = config.get('serverUrl');
|
|
542
|
+
return typeof serverUrl === 'string' && serverUrl ? serverUrl : null;
|
|
543
|
+
} catch {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function escapeRegExp(value) {
|
|
549
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function insertOrReplaceBlock(content, marker, newBlock) {
|
|
553
|
+
const markerEscaped = escapeRegExp(marker);
|
|
554
|
+
const regex = new RegExp(`${markerEscaped}[\\s\\S]*?(?=\\n## |\\n---\\n|$)`, '');
|
|
555
|
+
if (content.includes(marker)) {
|
|
556
|
+
return content.replace(regex, newBlock);
|
|
557
|
+
}
|
|
558
|
+
return content.trimEnd() + '\n\n' + newBlock + '\n';
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function updateInstructionFile(filePath, block, { createIfMissing = true } = {}) {
|
|
562
|
+
if (!createIfMissing && !existsSync(filePath)) return false;
|
|
563
|
+
let content = '';
|
|
564
|
+
if (existsSync(filePath)) {
|
|
565
|
+
content = readFileSync(filePath, 'utf8');
|
|
566
|
+
}
|
|
567
|
+
const next = insertOrReplaceBlock(content, DISTILL_MARKER, block);
|
|
568
|
+
writeFileSync(filePath, next);
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export async function syncBundleMetadata(options = {}) {
|
|
573
|
+
const homeDir = options.homeDir || homedir();
|
|
574
|
+
const serverUrl = options.serverUrl || readConfiguredServerUrl();
|
|
575
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
576
|
+
const log = options.log || (() => {});
|
|
577
|
+
|
|
578
|
+
if (!serverUrl) {
|
|
579
|
+
return { synced: false, reason: 'no-server-url' };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let manifest;
|
|
583
|
+
try {
|
|
584
|
+
const response = await fetchImpl(`${serverUrl}/api/downloads/bundles/manifest`);
|
|
585
|
+
if (!response?.ok) {
|
|
586
|
+
return { synced: false, reason: `http-${response?.status || 'unknown'}` };
|
|
587
|
+
}
|
|
588
|
+
manifest = await response.json();
|
|
589
|
+
} catch {
|
|
590
|
+
return { synced: false, reason: 'fetch-failed' };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!manifest || manifest.version !== 1) {
|
|
594
|
+
return { synced: false, reason: 'invalid-manifest' };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
mkdirSync(join(homeDir, '.termify'), { recursive: true });
|
|
598
|
+
writeFileSync(join(homeDir, '.termify', 'bundles-manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
599
|
+
|
|
600
|
+
if (manifest.distillDefaults) {
|
|
601
|
+
writeFileSync(join(homeDir, '.termify', 'distill.json'), JSON.stringify(manifest.distillDefaults, null, 2) + '\n');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const instructions = manifest.instructions?.distill;
|
|
605
|
+
if (instructions) {
|
|
606
|
+
updateInstructionFile(join(homeDir, '.codex', 'instructions.md'), instructions);
|
|
607
|
+
|
|
608
|
+
const geminiDir = join(homeDir, '.gemini');
|
|
609
|
+
if (existsSync(geminiDir)) {
|
|
610
|
+
updateInstructionFile(join(geminiDir, 'GEMINI.md'), instructions);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
updateInstructionFile(join(homeDir, '.claude', 'CLAUDE.md'), instructions, { createIfMissing: false });
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
log('[termify-agent] Bundle metadata refreshed');
|
|
617
|
+
return { synced: true, manifest };
|
|
618
|
+
}
|
|
619
|
+
|
|
523
620
|
// ---------------------------------------------------------------------------
|
|
524
621
|
// Symlink in /usr/local/bin for sudo access
|
|
525
622
|
// ---------------------------------------------------------------------------
|
|
@@ -717,6 +814,18 @@ async function main() {
|
|
|
717
814
|
s4.warn(`distill skipped ${DIM}(${err.message || 'setup failed'})${RESET}`);
|
|
718
815
|
}
|
|
719
816
|
|
|
817
|
+
const s5 = createSpinner('Refreshing bundle metadata...');
|
|
818
|
+
try {
|
|
819
|
+
const bundleSync = await syncBundleMetadata({ log: () => {} });
|
|
820
|
+
if (bundleSync.synced) {
|
|
821
|
+
s5.succeed('Bundle metadata refreshed');
|
|
822
|
+
} else {
|
|
823
|
+
s5.warn(`Bundle metadata skipped ${DIM}(${bundleSync.reason})${RESET}`);
|
|
824
|
+
}
|
|
825
|
+
} catch (err) {
|
|
826
|
+
s5.warn(`Bundle metadata skipped ${DIM}(${err.message || 'sync failed'})${RESET}`);
|
|
827
|
+
}
|
|
828
|
+
|
|
720
829
|
unmuteConsole();
|
|
721
830
|
|
|
722
831
|
console.log('');
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdirSync, lstatSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, lstatSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
|
|
7
|
-
import { detectGlobalWrapperState, ensureGlobalSymlink } from './postinstall.js';
|
|
7
|
+
import { detectGlobalWrapperState, ensureGlobalSymlink, syncBundleMetadata } from './postinstall.js';
|
|
8
8
|
|
|
9
9
|
function makeTempDir() {
|
|
10
10
|
return mkdtempSync(join(tmpdir(), 'termify-agent-postinstall-'));
|
|
@@ -94,3 +94,64 @@ test('detectGlobalWrapperState treats an up-to-date symlink as ready', (t) => {
|
|
|
94
94
|
assert.equal(detectGlobalWrapperState(target, desiredPath), 'ready');
|
|
95
95
|
assert.equal(readFileSync(desiredPath, 'utf8'), '#!/usr/bin/env node\n');
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
test('syncBundleMetadata refreshes the local bundle manifest and distill instructions on reinstall', async (t) => {
|
|
99
|
+
const tempDir = makeTempDir();
|
|
100
|
+
const homeDir = join(tempDir, 'home');
|
|
101
|
+
const codexDir = join(homeDir, '.codex');
|
|
102
|
+
const geminiDir = join(homeDir, '.gemini');
|
|
103
|
+
const claudeDir = join(homeDir, '.claude');
|
|
104
|
+
|
|
105
|
+
t.after(() => {
|
|
106
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
mkdirSync(codexDir, { recursive: true });
|
|
110
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
111
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
112
|
+
|
|
113
|
+
writeFileSync(join(codexDir, 'instructions.md'), '# Codex Instructions\n');
|
|
114
|
+
writeFileSync(join(geminiDir, 'GEMINI.md'), '# Gemini Instructions\n');
|
|
115
|
+
writeFileSync(join(claudeDir, 'CLAUDE.md'), '# Claude Instructions\n');
|
|
116
|
+
|
|
117
|
+
const manifest = {
|
|
118
|
+
version: 1,
|
|
119
|
+
generatedAt: '2026-03-08T00:11:59Z',
|
|
120
|
+
mcps: [],
|
|
121
|
+
hooks: [],
|
|
122
|
+
hookDefinitions: {},
|
|
123
|
+
mcpRegistrations: {},
|
|
124
|
+
instructions: {
|
|
125
|
+
distill: '## Distill\n\nUse `distill -- <command>` for token reduction.\n',
|
|
126
|
+
},
|
|
127
|
+
distillDefaults: {
|
|
128
|
+
primaryHost: 'https://termify.justdiego.com/api/ollama',
|
|
129
|
+
primaryModel: 'huihui_ai/qwen3.5-abliterated:2b',
|
|
130
|
+
timeoutMs: 10000,
|
|
131
|
+
thinking: false,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const result = await syncBundleMetadata({
|
|
136
|
+
homeDir,
|
|
137
|
+
serverUrl: 'https://termify.justdiego.com',
|
|
138
|
+
fetchImpl: async () => ({
|
|
139
|
+
ok: true,
|
|
140
|
+
json: async () => manifest,
|
|
141
|
+
}),
|
|
142
|
+
log: () => {},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.equal(result.synced, true);
|
|
146
|
+
|
|
147
|
+
const localManifestPath = join(homeDir, '.termify', 'bundles-manifest.json');
|
|
148
|
+
const distillConfigPath = join(homeDir, '.termify', 'distill.json');
|
|
149
|
+
|
|
150
|
+
assert.equal(existsSync(localManifestPath), true);
|
|
151
|
+
assert.equal(existsSync(distillConfigPath), true);
|
|
152
|
+
assert.match(readFileSync(join(codexDir, 'instructions.md'), 'utf8'), /^## Distill/m);
|
|
153
|
+
assert.match(readFileSync(join(geminiDir, 'GEMINI.md'), 'utf8'), /^## Distill/m);
|
|
154
|
+
assert.match(readFileSync(join(claudeDir, 'CLAUDE.md'), 'utf8'), /^## Distill/m);
|
|
155
|
+
assert.equal(JSON.parse(readFileSync(localManifestPath, 'utf8')).generatedAt, '2026-03-08T00:11:59Z');
|
|
156
|
+
assert.equal(JSON.parse(readFileSync(distillConfigPath, 'utf8')).primaryModel, 'huihui_ai/qwen3.5-abliterated:2b');
|
|
157
|
+
});
|