rigjs 3.0.32 → 4.0.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/.claude/skills/rig-wiki/SKILL.md +104 -0
- package/.claude-plugin/plugin.json +14 -0
- package/README.md +18 -1
- package/README_CN.md +17 -1
- package/RIG_CREW_SKILL.md +274 -0
- package/RIG_WIKI_SKILL.md +104 -0
- package/bin/rig.js +0 -0
- package/built/index.js +376 -299
- package/doc/architecture/README.md +139 -0
- package/doc/architecture/agents.md +180 -0
- package/doc/architecture/fc.md +17 -0
- package/doc/architecture/wiki.md +278 -0
- package/lib/crew/ask.ts +24 -0
- package/lib/crew/board.ts +123 -0
- package/lib/crew/config.ts +109 -0
- package/lib/crew/doctor.ts +40 -0
- package/lib/crew/inbox.ts +29 -0
- package/lib/crew/index.ts +108 -0
- package/lib/crew/init.ts +113 -0
- package/lib/crew/paths.ts +13 -0
- package/lib/crew/project.ts +84 -0
- package/lib/crew/role.ts +121 -0
- package/lib/crew/roleCommand.ts +150 -0
- package/lib/crew/state.ts +19 -0
- package/lib/crew/status.ts +27 -0
- package/lib/crew/stub.ts +9 -0
- package/lib/crew/sync.ts +15 -0
- package/lib/crew/task.ts +92 -0
- package/lib/crew/vault.ts +266 -0
- package/lib/installLocal.ts +189 -0
- package/lib/rig/index.ts +26 -3
- package/lib/tag/index.ts +1 -1
- package/lib/wiki/README.md +79 -0
- package/lib/wiki/agent/claude.ts +65 -0
- package/lib/wiki/agent/codex.ts +22 -0
- package/lib/wiki/agent/index.ts +11 -0
- package/lib/wiki/agent/list.ts +27 -0
- package/lib/wiki/agent/pi.ts +21 -0
- package/lib/wiki/agent/registry.ts +16 -0
- package/lib/wiki/agent/types.ts +37 -0
- package/lib/wiki/agent/use.ts +21 -0
- package/lib/wiki/config.ts +99 -0
- package/lib/wiki/daemon/index.ts +25 -0
- package/lib/wiki/daemon/install.ts +69 -0
- package/lib/wiki/daemon/logs.ts +16 -0
- package/lib/wiki/daemon/runner.ts +42 -0
- package/lib/wiki/daemon/start.ts +20 -0
- package/lib/wiki/daemon/status.ts +23 -0
- package/lib/wiki/daemon/stop.ts +16 -0
- package/lib/wiki/daemon/uninstall.ts +17 -0
- package/lib/wiki/db.ts +71 -0
- package/lib/wiki/fetch.ts +206 -0
- package/lib/wiki/index.ts +106 -0
- package/lib/wiki/indexCmd.ts +23 -0
- package/lib/wiki/ingest.ts +271 -0
- package/lib/wiki/init.ts +125 -0
- package/lib/wiki/installSkill.ts +92 -0
- package/lib/wiki/lint.ts +252 -0
- package/lib/wiki/list.ts +69 -0
- package/lib/wiki/pathGuard.ts +87 -0
- package/lib/wiki/paths.ts +29 -0
- package/lib/wiki/platform.ts +8 -0
- package/lib/wiki/qmd.ts +205 -0
- package/lib/wiki/query.ts +144 -0
- package/lib/wiki/rebuild.ts +56 -0
- package/lib/wiki/register.ts +94 -0
- package/lib/wiki/scan.ts +0 -0
- package/lib/wiki/uninstallSkill.ts +37 -0
- package/lib/wiki/unregister.ts +16 -0
- package/package.json +36 -6
- package/scripts/postinstall.mjs +108 -0
- package/scripts/publish.mjs +93 -0
- package/scripts/sync-skill.mjs +33 -0
- package/scripts/version-code.mjs +86 -0
- package/skills.md +54 -0
- package/.github/workflows/npm-publish.yml +0 -22
- package/demo/.env.oem1 +0 -4
- package/demo/.env.oem2 +0 -4
- package/demo/babel.config.js +0 -5
- package/demo/env.rig.json5 +0 -8
- package/demo/jsconfig.json +0 -19
- package/demo/package.json +0 -59
- package/demo/package.rig.json5 +0 -78
- package/demo/public/favicon.ico +0 -0
- package/demo/public/index.html +0 -17
- package/demo/rig_dev/.gitkeep +0 -0
- package/demo/rig_helper.d.ts +0 -4
- package/demo/rig_helper.js +0 -10
- package/demo/rigs/.gitkeep +0 -0
- package/demo/src/App.vue +0 -34
- package/demo/src/assets/logo.png +0 -0
- package/demo/src/components/HelloWorld.vue +0 -58
- package/demo/src/main.js +0 -8
- package/demo/vue.config.js +0 -8
- package/demo/yarn.lock +0 -6312
- package/develop.png +0 -0
- package/jest/test.rig.json5 +0 -14
- package/jest.config.ts +0 -16
- package/production.png +0 -0
- package/tsconfig.json +0 -53
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { AgentAdapter, AgentDetect, AgentRunOpts, AgentRunResult, NotImplementedError } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stub. Detection works; `run` will throw until the permission/whitelist API
|
|
6
|
+
* of the codex CLI is settled. Target P3.
|
|
7
|
+
*/
|
|
8
|
+
export class CodexAdapter implements AgentAdapter {
|
|
9
|
+
name = 'codex' as const;
|
|
10
|
+
|
|
11
|
+
async detect(): Promise<AgentDetect> {
|
|
12
|
+
const which = spawnSync('command', ['-v', 'codex'], { encoding: 'utf8', shell: '/bin/sh' });
|
|
13
|
+
const binPath = (which.stdout || '').trim();
|
|
14
|
+
if (which.status !== 0 || !binPath) return { installed: false };
|
|
15
|
+
const v = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
16
|
+
return { installed: true, path: binPath, version: (v.stdout || '').trim() || undefined };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async run(_opts: AgentRunOpts): Promise<AgentRunResult> {
|
|
20
|
+
throw new NotImplementedError('codex');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import agentList from './list';
|
|
2
|
+
import agentUse from './use';
|
|
3
|
+
|
|
4
|
+
export { adapters, getAdapter } from './registry';
|
|
5
|
+
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
export function registerAgentCommands(parent: any): void {
|
|
8
|
+
const agent = parent.command('agent').description('inspect / configure agent CLIs');
|
|
9
|
+
agent.command('list').description('list installed agent CLIs').action(agentList);
|
|
10
|
+
agent.command('use <name>').description('set default agent (claude|codex|pi)').action(agentUse);
|
|
11
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import print from '../../print';
|
|
2
|
+
import { adapters } from './registry';
|
|
3
|
+
import { loadRigConfig } from '../config';
|
|
4
|
+
|
|
5
|
+
export default async function agentList(): Promise<void> {
|
|
6
|
+
const defaultAgent = loadRigConfig().wiki?.defaultAgent || 'claude';
|
|
7
|
+
print.info('rig wiki — agent adapters');
|
|
8
|
+
for (const a of adapters) {
|
|
9
|
+
let row: string;
|
|
10
|
+
try {
|
|
11
|
+
const d = await a.detect();
|
|
12
|
+
row = d.installed
|
|
13
|
+
? ` ${star(a.name, defaultAgent)} ${a.name.padEnd(7)} installed ${d.version || '(no --version output)'} ${d.path || ''}`
|
|
14
|
+
: ` ${star(a.name, defaultAgent)} ${a.name.padEnd(7)} not installed`;
|
|
15
|
+
} catch (e: any) {
|
|
16
|
+
row = ` ${star(a.name, defaultAgent)} ${a.name.padEnd(7)} ERROR ${e.message}`;
|
|
17
|
+
}
|
|
18
|
+
// eslint-disable-next-line no-console
|
|
19
|
+
console.log(row);
|
|
20
|
+
}
|
|
21
|
+
// eslint-disable-next-line no-console
|
|
22
|
+
console.log(`\ndefault: ${defaultAgent} (change with: rig wiki agent use <name>)`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function star(name: string, def: string) {
|
|
26
|
+
return name === def ? '*' : ' ';
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { AgentAdapter, AgentDetect, AgentRunOpts, AgentRunResult, NotImplementedError } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stub. Target P3. Upstream CLI not yet fixed (Bo to specify which `pi`).
|
|
6
|
+
*/
|
|
7
|
+
export class PiAdapter implements AgentAdapter {
|
|
8
|
+
name = 'pi' as const;
|
|
9
|
+
|
|
10
|
+
async detect(): Promise<AgentDetect> {
|
|
11
|
+
const which = spawnSync('command', ['-v', 'pi'], { encoding: 'utf8', shell: '/bin/sh' });
|
|
12
|
+
const binPath = (which.stdout || '').trim();
|
|
13
|
+
if (which.status !== 0 || !binPath) return { installed: false };
|
|
14
|
+
const v = spawnSync('pi', ['--version'], { encoding: 'utf8' });
|
|
15
|
+
return { installed: true, path: binPath, version: (v.stdout || '').trim() || undefined };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async run(_opts: AgentRunOpts): Promise<AgentRunResult> {
|
|
19
|
+
throw new NotImplementedError('pi');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AgentAdapter, AgentName } from './types';
|
|
2
|
+
import { ClaudeAdapter } from './claude';
|
|
3
|
+
import { CodexAdapter } from './codex';
|
|
4
|
+
import { PiAdapter } from './pi';
|
|
5
|
+
|
|
6
|
+
export const adapters: AgentAdapter[] = [
|
|
7
|
+
new ClaudeAdapter(),
|
|
8
|
+
new CodexAdapter(),
|
|
9
|
+
new PiAdapter(),
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
export function getAdapter(name: AgentName): AgentAdapter {
|
|
13
|
+
const a = adapters.find(x => x.name === name);
|
|
14
|
+
if (!a) throw new Error(`unknown agent: ${name}`);
|
|
15
|
+
return a;
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type AgentName = 'claude' | 'codex' | 'pi';
|
|
2
|
+
|
|
3
|
+
export interface AgentDetect {
|
|
4
|
+
installed: boolean;
|
|
5
|
+
version?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentRunOpts {
|
|
10
|
+
prompt: string;
|
|
11
|
+
systemPrompt?: string;
|
|
12
|
+
files?: string[];
|
|
13
|
+
cwd: string;
|
|
14
|
+
allowWrite: boolean;
|
|
15
|
+
tools?: ('webfetch' | 'qmd' | 'bash')[];
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AgentRunResult {
|
|
20
|
+
ok: boolean;
|
|
21
|
+
stdout: string;
|
|
22
|
+
stderr: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
exitCode: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentAdapter {
|
|
28
|
+
name: AgentName;
|
|
29
|
+
detect(): Promise<AgentDetect>;
|
|
30
|
+
run(opts: AgentRunOpts): Promise<AgentRunResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class NotImplementedError extends Error {
|
|
34
|
+
constructor(adapter: string) {
|
|
35
|
+
super(`adapter "${adapter}" is not implemented yet (P3 roadmap)`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import print from '../../print';
|
|
2
|
+
import { loadRigConfig, saveRigConfig } from '../config';
|
|
3
|
+
import { AgentName } from './types';
|
|
4
|
+
|
|
5
|
+
const VALID: AgentName[] = ['claude', 'codex', 'pi'];
|
|
6
|
+
const IMPLEMENTED: AgentName[] = ['claude'];
|
|
7
|
+
|
|
8
|
+
export default function agentUse(name: string): void {
|
|
9
|
+
if (!VALID.includes(name as AgentName)) {
|
|
10
|
+
print.error(`unknown agent: ${name} (valid: ${VALID.join(', ')})`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (!IMPLEMENTED.includes(name as AgentName)) {
|
|
14
|
+
print.error(`adapter "${name}" is not implemented yet (P3 roadmap). Stick with "claude" for now.`);
|
|
15
|
+
process.exit(20);
|
|
16
|
+
}
|
|
17
|
+
const cfg = loadRigConfig();
|
|
18
|
+
cfg.wiki = { ...(cfg.wiki || {}), defaultAgent: name as AgentName };
|
|
19
|
+
saveRigConfig(cfg);
|
|
20
|
+
print.succeed(`default agent set to ${name}`);
|
|
21
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import JSON5 from 'json5';
|
|
4
|
+
import { paths } from './paths';
|
|
5
|
+
|
|
6
|
+
export interface RigConfig {
|
|
7
|
+
wiki?: {
|
|
8
|
+
defaultAgent?: 'claude' | 'codex' | 'pi';
|
|
9
|
+
qmd?: { enabled?: 'auto' | 'on' | 'off' };
|
|
10
|
+
logRotateMB?: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface WikiEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
path: string; // absolute path to wiki dir
|
|
17
|
+
project?: string; // absolute path to project root
|
|
18
|
+
include?: string[];
|
|
19
|
+
exclude?: string[];
|
|
20
|
+
schedule?: { scan?: string; lint?: string; ingest?: string | null };
|
|
21
|
+
ingestRules?: { match: string; mode: 'auto-on-new' | 'propose-only' }[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WikiConfig {
|
|
25
|
+
defaults?: {
|
|
26
|
+
schedule?: { scan?: string; lint?: string; ingest?: string | null };
|
|
27
|
+
};
|
|
28
|
+
wikis: WikiEntry[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_CONFIG: RigConfig = {
|
|
32
|
+
wiki: { defaultAgent: 'claude', qmd: { enabled: 'auto' }, logRotateMB: 50 },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DEFAULT_WIKI_CONFIG: WikiConfig = {
|
|
36
|
+
defaults: { schedule: { scan: '0 */6 * * *', lint: '0 3 * * *', ingest: null } },
|
|
37
|
+
wikis: [],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function ensureHomeDir() {
|
|
41
|
+
fs.mkdirSync(paths.home, { recursive: true });
|
|
42
|
+
fs.mkdirSync(paths.locks, { recursive: true });
|
|
43
|
+
fs.mkdirSync(paths.logs, { recursive: true });
|
|
44
|
+
fs.mkdirSync(paths.cache, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function readJson5<T>(file: string, fallback: T): T {
|
|
48
|
+
if (!fs.existsSync(file)) return fallback;
|
|
49
|
+
try {
|
|
50
|
+
return JSON5.parse(fs.readFileSync(file, 'utf8')) as T;
|
|
51
|
+
} catch (e: any) {
|
|
52
|
+
throw new Error(`failed to parse ${file}: ${e.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeJson5(file: string, data: unknown) {
|
|
57
|
+
fs.writeFileSync(file, JSON5.stringify(data, null, 2) + '\n', 'utf8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadRigConfig(): RigConfig {
|
|
61
|
+
ensureHomeDir();
|
|
62
|
+
return { ...DEFAULT_CONFIG, ...readJson5<RigConfig>(paths.config, DEFAULT_CONFIG) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function saveRigConfig(cfg: RigConfig) {
|
|
66
|
+
ensureHomeDir();
|
|
67
|
+
writeJson5(paths.config, cfg);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function loadWikiConfig(): WikiConfig {
|
|
71
|
+
ensureHomeDir();
|
|
72
|
+
const cfg = readJson5<WikiConfig>(paths.wikiConfig, DEFAULT_WIKI_CONFIG);
|
|
73
|
+
// backwards-compat: ensure wikis array exists
|
|
74
|
+
if (!Array.isArray(cfg.wikis)) cfg.wikis = [];
|
|
75
|
+
return { ...DEFAULT_WIKI_CONFIG, ...cfg };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function saveWikiConfig(cfg: WikiConfig) {
|
|
79
|
+
ensureHomeDir();
|
|
80
|
+
writeJson5(paths.wikiConfig, cfg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Find a wiki entry by name OR by absolute path. */
|
|
84
|
+
export function findWiki(cfg: WikiConfig, nameOrPath: string): WikiEntry | undefined {
|
|
85
|
+
return cfg.wikis.find(w => w.name === nameOrPath || w.path === path.resolve(nameOrPath));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve target wiki for a command:
|
|
90
|
+
* 1. If `--wiki <name>` provided, look up by name.
|
|
91
|
+
* 2. Else walk up from CWD; first match wins (by `project` or `path` prefix).
|
|
92
|
+
* 3. Return undefined if nothing matches.
|
|
93
|
+
*/
|
|
94
|
+
export function resolveWiki(cfg: WikiConfig, wikiFlag?: string): WikiEntry | undefined {
|
|
95
|
+
if (wikiFlag) return cfg.wikis.find(w => w.name === wikiFlag);
|
|
96
|
+
const cwd = process.cwd();
|
|
97
|
+
return cfg.wikis.find(w => cwd === w.path || cwd.startsWith(w.path + path.sep) ||
|
|
98
|
+
(w.project && (cwd === w.project || cwd.startsWith(w.project + path.sep))));
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import daemonInstall from './install';
|
|
2
|
+
import daemonUninstall from './uninstall';
|
|
3
|
+
import daemonStart from './start';
|
|
4
|
+
import daemonStop from './stop';
|
|
5
|
+
import daemonStatus from './status';
|
|
6
|
+
import daemonLogs from './logs';
|
|
7
|
+
import daemonRunner from './runner';
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export function registerDaemonCommands(parent: any): void {
|
|
11
|
+
const daemon = parent.command('daemon').description('launchd-managed background runner');
|
|
12
|
+
|
|
13
|
+
daemon.command('install').description('install ~/Library/LaunchAgents/ai.flashhand.rig.wiki.plist').action(daemonInstall);
|
|
14
|
+
daemon.command('uninstall').description('remove plist + bootout').action(daemonUninstall);
|
|
15
|
+
daemon.command('start').description('bootstrap launchd agent').action(daemonStart);
|
|
16
|
+
daemon.command('stop').description('bootout launchd agent').action(daemonStop);
|
|
17
|
+
daemon.command('status').description('print state + pid').action(daemonStatus);
|
|
18
|
+
daemon.command('logs')
|
|
19
|
+
.option('-f, --follow', 'tail -f')
|
|
20
|
+
.description('tail the daemon log')
|
|
21
|
+
.action(daemonLogs);
|
|
22
|
+
|
|
23
|
+
// hidden — launchctl invokes this; humans should never type it
|
|
24
|
+
daemon.command('runner').description('launchd entry — do not invoke directly').action(daemonRunner);
|
|
25
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import print from '../../print';
|
|
5
|
+
import { paths, daemonLabel } from '../paths';
|
|
6
|
+
import { requireMacOS } from '../platform';
|
|
7
|
+
|
|
8
|
+
function findRigEntry(): string {
|
|
9
|
+
// Walk up from this file to find the package root, then return built/index.js
|
|
10
|
+
let dir = __dirname;
|
|
11
|
+
for (let i = 0; i < 10; i++) {
|
|
12
|
+
const pkg = path.join(dir, 'package.json');
|
|
13
|
+
if (fs.existsSync(pkg)) {
|
|
14
|
+
try {
|
|
15
|
+
const p = JSON.parse(fs.readFileSync(pkg, 'utf8'));
|
|
16
|
+
if (p.name === 'rigjs') return path.join(dir, 'built', 'index.js');
|
|
17
|
+
} catch { /* keep walking */ }
|
|
18
|
+
}
|
|
19
|
+
const parent = path.dirname(dir);
|
|
20
|
+
if (parent === dir) break;
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
throw new Error('could not locate rigjs install root');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findNode(): string {
|
|
27
|
+
// Prefer /usr/local/bin/node (Homebrew Intel) then /opt/homebrew/bin/node (arm64).
|
|
28
|
+
for (const p of ['/usr/local/bin/node', '/opt/homebrew/bin/node']) {
|
|
29
|
+
if (fs.existsSync(p)) return p;
|
|
30
|
+
}
|
|
31
|
+
// Fall back to whichever node is invoking us right now.
|
|
32
|
+
return process.execPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function daemonInstall(): void {
|
|
36
|
+
requireMacOS();
|
|
37
|
+
fs.mkdirSync(path.dirname(paths.launchAgent), { recursive: true });
|
|
38
|
+
fs.mkdirSync(paths.logs, { recursive: true });
|
|
39
|
+
|
|
40
|
+
const nodeBin = findNode();
|
|
41
|
+
const rigEntry = findRigEntry();
|
|
42
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
43
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
44
|
+
<plist version="1.0"><dict>
|
|
45
|
+
<key>Label</key><string>${daemonLabel}</string>
|
|
46
|
+
<key>ProgramArguments</key><array>
|
|
47
|
+
<string>${nodeBin}</string>
|
|
48
|
+
<string>${rigEntry}</string>
|
|
49
|
+
<string>wiki</string><string>daemon</string><string>runner</string>
|
|
50
|
+
</array>
|
|
51
|
+
<key>RunAtLoad</key><true/>
|
|
52
|
+
<key>KeepAlive</key><true/>
|
|
53
|
+
<key>StandardOutPath</key><string>${paths.daemonLog}</string>
|
|
54
|
+
<key>StandardErrorPath</key><string>${paths.daemonLog}</string>
|
|
55
|
+
</dict></plist>
|
|
56
|
+
`;
|
|
57
|
+
fs.writeFileSync(paths.launchAgent, plist, 'utf8');
|
|
58
|
+
|
|
59
|
+
// bootstrap (idempotent: bootout first to allow re-install)
|
|
60
|
+
const uid = process.getuid?.() ?? 0;
|
|
61
|
+
spawnSync('launchctl', ['bootout', `gui/${uid}`, paths.launchAgent], { stdio: 'ignore' });
|
|
62
|
+
const boot = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, paths.launchAgent], { stdio: 'inherit' });
|
|
63
|
+
if (boot.status !== 0) {
|
|
64
|
+
print.error('launchctl bootstrap failed');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
print.succeed(`installed launchd agent ${daemonLabel}`);
|
|
68
|
+
print.info(`logs: ${paths.daemonLog}`);
|
|
69
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import print from '../../print';
|
|
4
|
+
import { paths } from '../paths';
|
|
5
|
+
|
|
6
|
+
interface LogsOpts { follow?: boolean; }
|
|
7
|
+
|
|
8
|
+
export default function daemonLogs(opts: LogsOpts): void {
|
|
9
|
+
if (!fs.existsSync(paths.daemonLog)) {
|
|
10
|
+
print.info(`no log yet at ${paths.daemonLog}`);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const args = opts.follow ? ['-f', paths.daemonLog] : [paths.daemonLog];
|
|
14
|
+
const child = spawn('tail', args, { stdio: 'inherit' });
|
|
15
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
16
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import print from '../../print';
|
|
2
|
+
import { loadWikiConfig } from '../config';
|
|
3
|
+
import { paths } from '../paths';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Daemon entry. launchctl invokes:
|
|
9
|
+
* node <rig-built>/index.js wiki daemon runner
|
|
10
|
+
*
|
|
11
|
+
* v1: minimal heartbeat loop that logs to ~/.rig/logs/wiki-daemon.log.
|
|
12
|
+
* P2 will add: cron-job registration per wiki for `schedule.scan` / `schedule.lint`,
|
|
13
|
+
* plus auto-on-new ingest from `ingestRules`.
|
|
14
|
+
*/
|
|
15
|
+
export default function daemonRunner(): void {
|
|
16
|
+
fs.mkdirSync(paths.logs, { recursive: true });
|
|
17
|
+
const log = (msg: string) =>
|
|
18
|
+
fs.appendFileSync(paths.daemonLog, `[${new Date().toISOString()}] ${msg}\n`);
|
|
19
|
+
|
|
20
|
+
log('rig wiki daemon: starting (v1 heartbeat-only)');
|
|
21
|
+
|
|
22
|
+
let cfg = loadWikiConfig();
|
|
23
|
+
log(`registered wikis: ${cfg.wikis.map(w => w.name).join(', ') || '(none)'}`);
|
|
24
|
+
|
|
25
|
+
// Reload config every 10 minutes so daemon doesn't need restart when wikis change.
|
|
26
|
+
setInterval(() => {
|
|
27
|
+
try {
|
|
28
|
+
cfg = loadWikiConfig();
|
|
29
|
+
log(`heartbeat — wikis=${cfg.wikis.length}`);
|
|
30
|
+
} catch (e: any) {
|
|
31
|
+
log(`heartbeat — config error: ${e.message}`);
|
|
32
|
+
}
|
|
33
|
+
}, 10 * 60 * 1000);
|
|
34
|
+
|
|
35
|
+
process.on('SIGTERM', () => { log('SIGTERM — shutting down'); process.exit(0); });
|
|
36
|
+
process.on('SIGINT', () => { log('SIGINT — shutting down'); process.exit(0); });
|
|
37
|
+
|
|
38
|
+
// The interval keeps the event loop alive.
|
|
39
|
+
print.info(`daemon up; logging to ${paths.daemonLog}`);
|
|
40
|
+
// eslint-disable-next-line no-void
|
|
41
|
+
void path; // unused-import placeholder, retained for future cron wiring
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import print from '../../print';
|
|
4
|
+
import { paths } from '../paths';
|
|
5
|
+
import { requireMacOS } from '../platform';
|
|
6
|
+
|
|
7
|
+
export default function daemonStart(): void {
|
|
8
|
+
requireMacOS();
|
|
9
|
+
if (!fs.existsSync(paths.launchAgent)) {
|
|
10
|
+
print.error('launchd agent not installed. Run `rig wiki daemon install` first.');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const uid = process.getuid?.() ?? 0;
|
|
14
|
+
const res = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, paths.launchAgent], { stdio: 'inherit' });
|
|
15
|
+
if (res.status !== 0 && res.status !== 17 /* already loaded */) {
|
|
16
|
+
print.error('launchctl bootstrap failed');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
print.succeed('daemon started');
|
|
20
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import print from '../../print';
|
|
4
|
+
import { paths, daemonLabel } from '../paths';
|
|
5
|
+
import { requireMacOS } from '../platform';
|
|
6
|
+
|
|
7
|
+
export default function daemonStatus(): void {
|
|
8
|
+
requireMacOS();
|
|
9
|
+
if (!fs.existsSync(paths.launchAgent)) {
|
|
10
|
+
print.info('not installed (run `rig wiki daemon install`)');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const uid = process.getuid?.() ?? 0;
|
|
14
|
+
const res = spawnSync('launchctl', ['print', `gui/${uid}/${daemonLabel}`], { encoding: 'utf8' });
|
|
15
|
+
if (res.status !== 0) {
|
|
16
|
+
print.warn('not running (installed but unloaded)');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const out = res.stdout || '';
|
|
20
|
+
const state = (out.match(/state\s*=\s*(\S+)/) || [])[1] || 'unknown';
|
|
21
|
+
const pid = (out.match(/pid\s*=\s*(\d+)/) || [])[1];
|
|
22
|
+
print.info(`state=${state}${pid ? ` pid=${pid}` : ''}`);
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import print from '../../print';
|
|
4
|
+
import { paths } from '../paths';
|
|
5
|
+
import { requireMacOS } from '../platform';
|
|
6
|
+
|
|
7
|
+
export default function daemonStop(): void {
|
|
8
|
+
requireMacOS();
|
|
9
|
+
if (!fs.existsSync(paths.launchAgent)) {
|
|
10
|
+
print.info('no launchd agent installed');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const uid = process.getuid?.() ?? 0;
|
|
14
|
+
spawnSync('launchctl', ['bootout', `gui/${uid}`, paths.launchAgent], { stdio: 'inherit' });
|
|
15
|
+
print.succeed('daemon stopped');
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import print from '../../print';
|
|
4
|
+
import { paths } from '../paths';
|
|
5
|
+
import { requireMacOS } from '../platform';
|
|
6
|
+
|
|
7
|
+
export default function daemonUninstall(): void {
|
|
8
|
+
requireMacOS();
|
|
9
|
+
const uid = process.getuid?.() ?? 0;
|
|
10
|
+
if (fs.existsSync(paths.launchAgent)) {
|
|
11
|
+
spawnSync('launchctl', ['bootout', `gui/${uid}`, paths.launchAgent], { stdio: 'ignore' });
|
|
12
|
+
fs.rmSync(paths.launchAgent);
|
|
13
|
+
print.succeed(`removed ${paths.launchAgent}`);
|
|
14
|
+
} else {
|
|
15
|
+
print.info('no launchd agent installed');
|
|
16
|
+
}
|
|
17
|
+
}
|
package/lib/wiki/db.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { paths } from './paths';
|
|
3
|
+
|
|
4
|
+
// Lazy load so unit tests that don't touch the DB don't need the native binary.
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
type Database = any;
|
|
7
|
+
let cached: Database | null = null;
|
|
8
|
+
|
|
9
|
+
export function getDb(): Database {
|
|
10
|
+
if (cached) return cached;
|
|
11
|
+
fs.mkdirSync(paths.home, { recursive: true });
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
13
|
+
const BetterSqlite3 = require('better-sqlite3');
|
|
14
|
+
const db = new BetterSqlite3(paths.stateDb);
|
|
15
|
+
db.pragma('journal_mode = WAL');
|
|
16
|
+
db.pragma('foreign_keys = ON');
|
|
17
|
+
migrate(db);
|
|
18
|
+
cached = db;
|
|
19
|
+
return db;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function closeDb() {
|
|
23
|
+
if (cached) {
|
|
24
|
+
try { cached.close(); } catch { /* ignore */ }
|
|
25
|
+
cached = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function migrate(db: Database): void {
|
|
30
|
+
db.exec(`
|
|
31
|
+
CREATE TABLE IF NOT EXISTS source_sha (
|
|
32
|
+
wiki TEXT NOT NULL,
|
|
33
|
+
path TEXT NOT NULL,
|
|
34
|
+
sha TEXT NOT NULL,
|
|
35
|
+
mtime INTEGER NOT NULL,
|
|
36
|
+
PRIMARY KEY (wiki, path)
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS last_run (
|
|
39
|
+
wiki TEXT NOT NULL,
|
|
40
|
+
op TEXT NOT NULL,
|
|
41
|
+
ts INTEGER NOT NULL,
|
|
42
|
+
exit_code INTEGER NOT NULL,
|
|
43
|
+
PRIMARY KEY (wiki, op)
|
|
44
|
+
);
|
|
45
|
+
CREATE TABLE IF NOT EXISTS ingest_log (
|
|
46
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
wiki TEXT NOT NULL,
|
|
48
|
+
source_path TEXT NOT NULL,
|
|
49
|
+
ts INTEGER NOT NULL,
|
|
50
|
+
diff_hash TEXT NOT NULL,
|
|
51
|
+
applied INTEGER NOT NULL
|
|
52
|
+
);
|
|
53
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS pages_fts5 USING fts5(
|
|
54
|
+
wiki, slug, body, tokenize='unicode61 remove_diacritics 2'
|
|
55
|
+
);
|
|
56
|
+
`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function recordLastRun(wiki: string, op: string, exitCode: number): void {
|
|
60
|
+
const db = getDb();
|
|
61
|
+
db.prepare(
|
|
62
|
+
`INSERT INTO last_run (wiki, op, ts, exit_code) VALUES (?, ?, ?, ?)
|
|
63
|
+
ON CONFLICT(wiki, op) DO UPDATE SET ts = excluded.ts, exit_code = excluded.exit_code`
|
|
64
|
+
).run(wiki, op, Date.now(), exitCode);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getLastRun(wiki: string, op: string): { ts: number; exit_code: number } | undefined {
|
|
68
|
+
return getDb()
|
|
69
|
+
.prepare('SELECT ts, exit_code FROM last_run WHERE wiki = ? AND op = ?')
|
|
70
|
+
.get(wiki, op) as { ts: number; exit_code: number } | undefined;
|
|
71
|
+
}
|