@drewpayment/mink 0.6.1 → 0.8.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 +19 -0
- package/agents/mink-agent.md.tmpl +84 -0
- package/dashboard/out/404.html +1 -1
- package/dashboard/out/action-log.html +1 -1
- package/dashboard/out/action-log.txt +1 -1
- package/dashboard/out/activity.html +1 -1
- package/dashboard/out/activity.txt +1 -1
- package/dashboard/out/bugs.html +1 -1
- package/dashboard/out/bugs.txt +1 -1
- package/dashboard/out/capture.html +1 -1
- package/dashboard/out/capture.txt +1 -1
- package/dashboard/out/config.html +1 -1
- package/dashboard/out/config.txt +1 -1
- package/dashboard/out/daemon.html +1 -1
- package/dashboard/out/daemon.txt +1 -1
- package/dashboard/out/design.html +1 -1
- package/dashboard/out/design.txt +1 -1
- package/dashboard/out/discord.html +1 -1
- package/dashboard/out/discord.txt +1 -1
- package/dashboard/out/file-index.html +1 -1
- package/dashboard/out/file-index.txt +1 -1
- package/dashboard/out/index.html +1 -1
- package/dashboard/out/index.txt +1 -1
- package/dashboard/out/insights.html +1 -1
- package/dashboard/out/insights.txt +1 -1
- package/dashboard/out/learning.html +1 -1
- package/dashboard/out/learning.txt +1 -1
- package/dashboard/out/overview.html +1 -1
- package/dashboard/out/overview.txt +1 -1
- package/dashboard/out/scheduler.html +1 -1
- package/dashboard/out/scheduler.txt +1 -1
- package/dashboard/out/sync.html +1 -1
- package/dashboard/out/sync.txt +1 -1
- package/dashboard/out/tokens.html +1 -1
- package/dashboard/out/tokens.txt +1 -1
- package/dashboard/out/waste.html +1 -1
- package/dashboard/out/waste.txt +1 -1
- package/dashboard/out/wiki.html +1 -1
- package/dashboard/out/wiki.txt +1 -1
- package/dist/cli.js +804 -376
- package/package.json +2 -1
- package/src/cli.ts +8 -1
- package/src/commands/agent.ts +245 -0
- package/src/commands/daemon.ts +12 -1
- package/src/commands/init.ts +27 -0
- package/src/core/daemon-service.ts +227 -0
- /package/dashboard/out/_next/static/{FiL3S_40BA764FL66DRZV → EC-_8nIOf1GnPrIqZ7Mk3}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{FiL3S_40BA764FL66DRZV → EC-_8nIOf1GnPrIqZ7Mk3}/_ssgManifest.js +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drewpayment/mink",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A hidden presence that moves alongside the developer — token efficiency and cross-project wiki for AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"src/**/*.ts",
|
|
20
20
|
"dist/cli.js",
|
|
21
21
|
"skills/**/*",
|
|
22
|
+
"agents/**/*",
|
|
22
23
|
"dashboard/out"
|
|
23
24
|
],
|
|
24
25
|
"publishConfig": {
|
package/src/cli.ts
CHANGED
|
@@ -143,6 +143,12 @@ switch (command) {
|
|
|
143
143
|
break;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
case "agent": {
|
|
147
|
+
const { agent } = await import("./commands/agent");
|
|
148
|
+
await agent(cwd, process.argv.slice(3));
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
146
152
|
case "sync": {
|
|
147
153
|
const { sync } = await import("./commands/sync");
|
|
148
154
|
await sync(process.argv.slice(3));
|
|
@@ -213,6 +219,7 @@ switch (command) {
|
|
|
213
219
|
console.log(" note list [filters] List notes (--category, --tag, --recent)");
|
|
214
220
|
console.log(" note search <term> Full-text search across the vault");
|
|
215
221
|
console.log(" skill install Install /mink:note skill for Claude Code");
|
|
222
|
+
console.log(" agent Open a Claude Code session with the mink-agent persona");
|
|
216
223
|
console.log();
|
|
217
224
|
console.log("Devices & Sync:");
|
|
218
225
|
console.log(" device Show current device info");
|
|
@@ -235,7 +242,7 @@ switch (command) {
|
|
|
235
242
|
console.log();
|
|
236
243
|
console.log("Automation & Analysis:");
|
|
237
244
|
console.log(" dashboard [--port=N] Open the real-time web dashboard");
|
|
238
|
-
console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs)");
|
|
245
|
+
console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs|install|uninstall)");
|
|
239
246
|
console.log(" cron <cmd> [id] Manage scheduled tasks (list|run|retry)");
|
|
240
247
|
console.log(" update [options] Update Mink across registered projects");
|
|
241
248
|
console.log(" restore [backup] Restore state from a backup");
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { join, resolve, dirname } from "path";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "fs";
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import { minkRoot } from "../core/paths";
|
|
12
|
+
import { resolveVaultPath } from "../core/vault";
|
|
13
|
+
|
|
14
|
+
const AGENT_NAME = "mink-agent";
|
|
15
|
+
const TEMPLATE_FILE = `${AGENT_NAME}.md.tmpl`;
|
|
16
|
+
const INSTALLED_FILE = `${AGENT_NAME}.md`;
|
|
17
|
+
|
|
18
|
+
function getAgentTemplatePath(): string {
|
|
19
|
+
let dir = dirname(new URL(import.meta.url).pathname);
|
|
20
|
+
while (true) {
|
|
21
|
+
if (
|
|
22
|
+
existsSync(join(dir, "package.json")) &&
|
|
23
|
+
existsSync(join(dir, "agents", TEMPLATE_FILE))
|
|
24
|
+
) {
|
|
25
|
+
return join(dir, "agents", TEMPLATE_FILE);
|
|
26
|
+
}
|
|
27
|
+
const parent = dirname(dir);
|
|
28
|
+
if (parent === dir) break;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
return resolve(
|
|
32
|
+
dirname(new URL(import.meta.url).pathname),
|
|
33
|
+
"../../agents",
|
|
34
|
+
TEMPLATE_FILE
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getMinkVersion(): string {
|
|
39
|
+
let dir = dirname(new URL(import.meta.url).pathname);
|
|
40
|
+
while (true) {
|
|
41
|
+
const pkgPath = join(dir, "package.json");
|
|
42
|
+
if (existsSync(pkgPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
45
|
+
if (pkg.name && pkg.version) return pkg.version;
|
|
46
|
+
} catch {
|
|
47
|
+
// fall through
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const parent = dirname(dir);
|
|
51
|
+
if (parent === dir) break;
|
|
52
|
+
dir = parent;
|
|
53
|
+
}
|
|
54
|
+
return "unknown";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderTemplate(template: string, vars: Record<string, string>): string {
|
|
58
|
+
let out = template;
|
|
59
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
60
|
+
out = out.split(`{{${key}}}`).join(value);
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sha256(text: string): string {
|
|
66
|
+
return createHash("sha256").update(text).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function claudeAgentsDir(): string {
|
|
70
|
+
return join(homedir(), ".claude", "agents");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function installedAgentPath(): string {
|
|
74
|
+
return join(claudeAgentsDir(), INSTALLED_FILE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface InstallResult {
|
|
78
|
+
action: "installed" | "updated" | "unchanged" | "skipped";
|
|
79
|
+
path: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function installAgentDefinition(opts: { force: boolean; skip: boolean }): InstallResult {
|
|
83
|
+
const templatePath = getAgentTemplatePath();
|
|
84
|
+
if (!existsSync(templatePath)) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`[mink agent] bundled agent template not found at ${templatePath}\n` +
|
|
87
|
+
" This usually means the package was installed without bundled assets."
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const installed = installedAgentPath();
|
|
92
|
+
|
|
93
|
+
if (opts.skip && existsSync(installed)) {
|
|
94
|
+
return { action: "skipped", path: installed };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const template = readFileSync(templatePath, "utf-8");
|
|
98
|
+
const rendered = renderTemplate(template, {
|
|
99
|
+
MINK_ROOT: minkRoot(),
|
|
100
|
+
VAULT_PATH: resolveVaultPath(),
|
|
101
|
+
MINK_VERSION: getMinkVersion(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const exists = existsSync(installed);
|
|
105
|
+
if (!opts.force && exists) {
|
|
106
|
+
const current = readFileSync(installed, "utf-8");
|
|
107
|
+
if (sha256(current) === sha256(rendered)) {
|
|
108
|
+
return { action: "unchanged", path: installed };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
mkdirSync(claudeAgentsDir(), { recursive: true });
|
|
113
|
+
writeFileSync(installed, rendered);
|
|
114
|
+
return {
|
|
115
|
+
action: exists ? "updated" : "installed",
|
|
116
|
+
path: installed,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isClaudeOnPath(): boolean {
|
|
121
|
+
const result = spawnSync("claude", ["--version"], {
|
|
122
|
+
stdio: "ignore",
|
|
123
|
+
});
|
|
124
|
+
return !result.error && result.status === 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface ParsedArgs {
|
|
128
|
+
noUpdate: boolean;
|
|
129
|
+
reinstall: boolean;
|
|
130
|
+
passthrough: string[];
|
|
131
|
+
showHelp: boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseArgs(args: string[]): ParsedArgs {
|
|
135
|
+
const out: ParsedArgs = {
|
|
136
|
+
noUpdate: false,
|
|
137
|
+
reinstall: false,
|
|
138
|
+
passthrough: [],
|
|
139
|
+
showHelp: false,
|
|
140
|
+
};
|
|
141
|
+
let inPassthrough = false;
|
|
142
|
+
for (const arg of args) {
|
|
143
|
+
if (inPassthrough) {
|
|
144
|
+
out.passthrough.push(arg);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (arg === "--") {
|
|
148
|
+
inPassthrough = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (arg === "--no-update") {
|
|
152
|
+
out.noUpdate = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (arg === "--reinstall") {
|
|
156
|
+
out.reinstall = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (arg === "--help" || arg === "-h") {
|
|
160
|
+
out.showHelp = true;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
out.passthrough.push(arg);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function printHelp(): void {
|
|
169
|
+
console.log("Usage: mink agent [options] [-- <claude args...>]");
|
|
170
|
+
console.log();
|
|
171
|
+
console.log("Open an interactive Claude Code session in your mink home with");
|
|
172
|
+
console.log("the mink-agent persona — a proactive note/wiki assistant.");
|
|
173
|
+
console.log();
|
|
174
|
+
console.log("Options:");
|
|
175
|
+
console.log(" --no-update Don't refresh ~/.claude/agents/mink-agent.md if it exists");
|
|
176
|
+
console.log(" --reinstall Force overwrite the installed agent definition");
|
|
177
|
+
console.log(" -- <args> Forward remaining arguments to `claude`");
|
|
178
|
+
console.log();
|
|
179
|
+
console.log("Environment:");
|
|
180
|
+
console.log(" MINK_AGENT_NO_UPDATE=1 Equivalent to --no-update");
|
|
181
|
+
console.log();
|
|
182
|
+
console.log("The agent is bound to your mink root and resolved vault path. Changing");
|
|
183
|
+
console.log("`mink config wiki.path` triggers a refresh on the next launch.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function agent(_cwd: string, rawArgs: string[]): Promise<void> {
|
|
187
|
+
const args = parseArgs(rawArgs);
|
|
188
|
+
|
|
189
|
+
if (args.showHelp) {
|
|
190
|
+
printHelp();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const skipUpdate = args.noUpdate || process.env.MINK_AGENT_NO_UPDATE === "1";
|
|
195
|
+
|
|
196
|
+
const root = minkRoot();
|
|
197
|
+
if (!existsSync(root)) {
|
|
198
|
+
mkdirSync(root, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let result: InstallResult;
|
|
202
|
+
try {
|
|
203
|
+
result = installAgentDefinition({
|
|
204
|
+
force: args.reinstall,
|
|
205
|
+
skip: skipUpdate && !args.reinstall,
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
209
|
+
console.error(msg);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
switch (result.action) {
|
|
214
|
+
case "installed":
|
|
215
|
+
console.log(`[mink] installed mink-agent definition (v${getMinkVersion()}) -> ${result.path}`);
|
|
216
|
+
break;
|
|
217
|
+
case "updated":
|
|
218
|
+
console.log(`[mink] updated mink-agent definition -> ${result.path}`);
|
|
219
|
+
break;
|
|
220
|
+
case "unchanged":
|
|
221
|
+
case "skipped":
|
|
222
|
+
// silent
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isClaudeOnPath()) {
|
|
227
|
+
console.error("[mink agent] `claude` (Claude Code CLI) was not found on PATH.");
|
|
228
|
+
console.error(" Install Claude Code: https://claude.com/claude-code");
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const claudeArgs = ["--agent", AGENT_NAME, ...args.passthrough];
|
|
233
|
+
const child = spawnSync("claude", claudeArgs, {
|
|
234
|
+
cwd: root,
|
|
235
|
+
stdio: "inherit",
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
if (child.error) {
|
|
239
|
+
console.error(`[mink agent] failed to launch claude: ${child.error.message}`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
if (typeof child.status === "number") {
|
|
243
|
+
process.exit(child.status);
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/commands/daemon.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "fs";
|
|
2
2
|
import { startDaemon, stopDaemon } from "../core/daemon";
|
|
3
|
+
import { installService, uninstallService } from "../core/daemon-service";
|
|
3
4
|
import { schedulerLogPath } from "../core/paths";
|
|
4
5
|
|
|
5
6
|
export async function daemon(cwd: string, args: string[]): Promise<void> {
|
|
@@ -36,11 +37,21 @@ export async function daemon(cwd: string, args: string[]): Promise<void> {
|
|
|
36
37
|
break;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
case "install":
|
|
41
|
+
installService({ force: args.includes("--force") });
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case "uninstall":
|
|
45
|
+
uninstallService();
|
|
46
|
+
break;
|
|
47
|
+
|
|
39
48
|
default:
|
|
40
49
|
console.error(
|
|
41
50
|
`[mink] unknown daemon subcommand: ${subcommand ?? "(none)"}`
|
|
42
51
|
);
|
|
43
|
-
console.error(
|
|
52
|
+
console.error(
|
|
53
|
+
"Usage: mink daemon <start|stop|restart|logs|install|uninstall>"
|
|
54
|
+
);
|
|
44
55
|
process.exit(1);
|
|
45
56
|
}
|
|
46
57
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -106,6 +106,30 @@ function isMinkHook(entry: HookEntry | Record<string, unknown>): boolean {
|
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
const MINK_RULE_CONTENT = `---
|
|
110
|
+
description: Mink context management — automatic via hooks
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
This project uses **Mink** (\`@drewpayment/mink\`) for cross-session context management.
|
|
114
|
+
|
|
115
|
+
## How it works
|
|
116
|
+
- Mink runs automatically through Claude Code hooks configured in \`.claude/settings.json\` (SessionStart, PreToolUse, PostToolUse, Stop).
|
|
117
|
+
- All state lives in \`~/.mink/\` on the user's machine — **not** in this repository. Do not create or write to any in-repo state directory (no \`.wolf/\`, \`.mink/\`, etc.).
|
|
118
|
+
- Read intelligence, write enforcement, bug memory, and the token ledger are handled by the hooks. You do not need to manually read or update any state files.
|
|
119
|
+
|
|
120
|
+
## When to act on Mink
|
|
121
|
+
- If the user asks to "save a note", "remember this", "log this to my wiki", or similar, use the \`mink-note\` skill — it captures into the user's \`~/.mink/\` vault.
|
|
122
|
+
- If a hook surfaces a learning, past bug, or repeat-read warning, treat that as authoritative project memory and follow it.
|
|
123
|
+
- The \`mink dashboard\` and \`mink agent\` commands are user tools — do not invoke them on the user's behalf.
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
export function writeMinkRule(cwd: string): string {
|
|
127
|
+
const rulePath = resolve(cwd, ".claude", "rules", "mink.md");
|
|
128
|
+
mkdirSync(dirname(rulePath), { recursive: true });
|
|
129
|
+
atomicWriteText(rulePath, MINK_RULE_CONTENT);
|
|
130
|
+
return rulePath;
|
|
131
|
+
}
|
|
132
|
+
|
|
109
133
|
export function mergeHooksIntoSettings(
|
|
110
134
|
settingsPath: string,
|
|
111
135
|
newHooks: HooksConfig
|
|
@@ -148,6 +172,7 @@ export async function init(cwd: string): Promise<void> {
|
|
|
148
172
|
}
|
|
149
173
|
|
|
150
174
|
mergeHooksIntoSettings(settingsPath, hooks);
|
|
175
|
+
const rulePath = writeMinkRule(cwd);
|
|
151
176
|
|
|
152
177
|
mkdirSync(dir, { recursive: true });
|
|
153
178
|
|
|
@@ -173,12 +198,14 @@ export async function init(cwd: string): Promise<void> {
|
|
|
173
198
|
console.log(`[mink] upgrade complete`);
|
|
174
199
|
console.log(` project: ${projectId}`);
|
|
175
200
|
console.log(` hooks: ${settingsPath}`);
|
|
201
|
+
console.log(` rule: ${rulePath}`);
|
|
176
202
|
} else {
|
|
177
203
|
console.log(`[mink] initialized`);
|
|
178
204
|
console.log(` project: ${projectId}`);
|
|
179
205
|
console.log(` state: ${dir}`);
|
|
180
206
|
console.log(` runtime: ${runtime}`);
|
|
181
207
|
console.log(` hooks: ${settingsPath}`);
|
|
208
|
+
console.log(` rule: ${rulePath}`);
|
|
182
209
|
}
|
|
183
210
|
|
|
184
211
|
// Run initial scan
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join, resolve } from "path";
|
|
5
|
+
import { resolveCliPath } from "../commands/init";
|
|
6
|
+
|
|
7
|
+
export type ServicePlatform = "systemd" | "launchd";
|
|
8
|
+
|
|
9
|
+
export interface ServiceInvocation {
|
|
10
|
+
/** Absolute path to the executable used in ExecStart / ProgramArguments[0]. */
|
|
11
|
+
executable: string;
|
|
12
|
+
/** Arguments following the executable (e.g. ["daemon", "start"] or ["<cli.js>", "daemon", "start"]). */
|
|
13
|
+
args: string[];
|
|
14
|
+
/** Directory that should be added to PATH for the service's environment. */
|
|
15
|
+
pathDir: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ServicePaths {
|
|
19
|
+
unitFile: string;
|
|
20
|
+
unitDir: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function detectPlatform(): ServicePlatform | null {
|
|
24
|
+
if (process.platform === "linux") return "systemd";
|
|
25
|
+
if (process.platform === "darwin") return "launchd";
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve how the service should invoke mink.
|
|
31
|
+
*
|
|
32
|
+
* Prefer argv[1] when it is a bin shim (no .js/.ts extension) — that is the
|
|
33
|
+
* stable, install-method-agnostic entry point (e.g. ~/.bun/bin/mink). Fall
|
|
34
|
+
* back to invoking the compiled bundle via the current interpreter when
|
|
35
|
+
* running from source or from a non-shim entry.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveServiceInvocation(): ServiceInvocation {
|
|
38
|
+
const entry = process.argv[1];
|
|
39
|
+
if (entry && !/\.(js|ts|mjs|cjs)$/.test(entry) && existsSync(entry)) {
|
|
40
|
+
return {
|
|
41
|
+
executable: entry,
|
|
42
|
+
args: ["daemon", "start"],
|
|
43
|
+
pathDir: dirname(entry),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cliPath = resolveCliPath();
|
|
48
|
+
const interpreter = process.execPath;
|
|
49
|
+
return {
|
|
50
|
+
executable: interpreter,
|
|
51
|
+
args: [cliPath, "daemon", "start"],
|
|
52
|
+
pathDir: dirname(interpreter),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function servicePaths(platform: ServicePlatform): ServicePaths {
|
|
57
|
+
const home = homedir();
|
|
58
|
+
if (platform === "systemd") {
|
|
59
|
+
const unitDir = join(home, ".config", "systemd", "user");
|
|
60
|
+
return { unitDir, unitFile: join(unitDir, "mink-daemon.service") };
|
|
61
|
+
}
|
|
62
|
+
const unitDir = join(home, "Library", "LaunchAgents");
|
|
63
|
+
return { unitDir, unitFile: join(unitDir, "com.mink.daemon.plist") };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Build a systemd user unit file for the mink daemon. */
|
|
67
|
+
export function renderSystemdUnit(inv: ServiceInvocation): string {
|
|
68
|
+
const execStart = [inv.executable, ...inv.args].join(" ");
|
|
69
|
+
const stopArgs = inv.args.map((a) => (a === "start" ? "stop" : a));
|
|
70
|
+
const execStop = [inv.executable, ...stopArgs].join(" ");
|
|
71
|
+
const pathEnv = `${inv.pathDir}:/usr/local/bin:/usr/bin:/bin`;
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
"[Unit]",
|
|
75
|
+
"Description=Mink background daemon",
|
|
76
|
+
"After=network-online.target",
|
|
77
|
+
"Wants=network-online.target",
|
|
78
|
+
"",
|
|
79
|
+
"[Service]",
|
|
80
|
+
"Type=forking",
|
|
81
|
+
`ExecStart=${execStart}`,
|
|
82
|
+
`ExecStop=${execStop}`,
|
|
83
|
+
"Restart=on-failure",
|
|
84
|
+
"RestartSec=10",
|
|
85
|
+
`Environment="PATH=${pathEnv}"`,
|
|
86
|
+
"",
|
|
87
|
+
"[Install]",
|
|
88
|
+
"WantedBy=default.target",
|
|
89
|
+
"",
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Build a launchd user agent plist for the mink daemon. */
|
|
94
|
+
export function renderLaunchdPlist(inv: ServiceInvocation, logPath: string): string {
|
|
95
|
+
const programArgs = [inv.executable, ...inv.args]
|
|
96
|
+
.map((a) => ` <string>${escapeXml(a)}</string>`)
|
|
97
|
+
.join("\n");
|
|
98
|
+
const pathEnv = `${inv.pathDir}:/usr/local/bin:/usr/bin:/bin`;
|
|
99
|
+
|
|
100
|
+
return [
|
|
101
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
102
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
103
|
+
'<plist version="1.0">',
|
|
104
|
+
"<dict>",
|
|
105
|
+
" <key>Label</key>",
|
|
106
|
+
" <string>com.mink.daemon</string>",
|
|
107
|
+
" <key>ProgramArguments</key>",
|
|
108
|
+
" <array>",
|
|
109
|
+
programArgs,
|
|
110
|
+
" </array>",
|
|
111
|
+
" <key>RunAtLoad</key>",
|
|
112
|
+
" <true/>",
|
|
113
|
+
" <key>KeepAlive</key>",
|
|
114
|
+
" <dict>",
|
|
115
|
+
" <key>SuccessfulExit</key>",
|
|
116
|
+
" <false/>",
|
|
117
|
+
" </dict>",
|
|
118
|
+
" <key>EnvironmentVariables</key>",
|
|
119
|
+
" <dict>",
|
|
120
|
+
" <key>PATH</key>",
|
|
121
|
+
` <string>${escapeXml(pathEnv)}</string>`,
|
|
122
|
+
" </dict>",
|
|
123
|
+
" <key>StandardOutPath</key>",
|
|
124
|
+
` <string>${escapeXml(logPath)}</string>`,
|
|
125
|
+
" <key>StandardErrorPath</key>",
|
|
126
|
+
` <string>${escapeXml(logPath)}</string>`,
|
|
127
|
+
"</dict>",
|
|
128
|
+
"</plist>",
|
|
129
|
+
"",
|
|
130
|
+
].join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function escapeXml(s: string): string {
|
|
134
|
+
return s
|
|
135
|
+
.replace(/&/g, "&")
|
|
136
|
+
.replace(/</g, "<")
|
|
137
|
+
.replace(/>/g, ">")
|
|
138
|
+
.replace(/"/g, """);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface InstallOptions {
|
|
142
|
+
force?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function installService(options: InstallOptions = {}): void {
|
|
146
|
+
const platform = detectPlatform();
|
|
147
|
+
if (!platform) {
|
|
148
|
+
console.error(
|
|
149
|
+
`[mink] daemon install is not supported on ${process.platform} (supported: linux, darwin)`
|
|
150
|
+
);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const paths = servicePaths(platform);
|
|
155
|
+
if (existsSync(paths.unitFile) && !options.force) {
|
|
156
|
+
console.error(`[mink] unit file already exists: ${paths.unitFile}`);
|
|
157
|
+
console.error(
|
|
158
|
+
" re-run with --force to overwrite, or run `mink daemon uninstall` first"
|
|
159
|
+
);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const inv = resolveServiceInvocation();
|
|
164
|
+
mkdirSync(paths.unitDir, { recursive: true });
|
|
165
|
+
|
|
166
|
+
if (platform === "systemd") {
|
|
167
|
+
writeFileSync(paths.unitFile, renderSystemdUnit(inv));
|
|
168
|
+
try {
|
|
169
|
+
execSync("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
170
|
+
} catch {
|
|
171
|
+
// systemctl may be unavailable (e.g. CI, WSL1) — the file is still written.
|
|
172
|
+
}
|
|
173
|
+
console.log(`[mink] wrote ${paths.unitFile}`);
|
|
174
|
+
console.log("[mink] next steps:");
|
|
175
|
+
console.log(" systemctl --user enable --now mink-daemon.service");
|
|
176
|
+
console.log(" # To survive logout (one-time, requires sudo):");
|
|
177
|
+
console.log(` sudo loginctl enable-linger ${process.env.USER ?? "$USER"}`);
|
|
178
|
+
} else {
|
|
179
|
+
const { schedulerLogPath } = require("./paths") as typeof import("./paths");
|
|
180
|
+
writeFileSync(paths.unitFile, renderLaunchdPlist(inv, schedulerLogPath()));
|
|
181
|
+
console.log(`[mink] wrote ${paths.unitFile}`);
|
|
182
|
+
console.log("[mink] next steps:");
|
|
183
|
+
console.log(` launchctl load -w ${paths.unitFile}`);
|
|
184
|
+
console.log(" # Launch agents run automatically on login; no lingering needed.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function uninstallService(): void {
|
|
189
|
+
const platform = detectPlatform();
|
|
190
|
+
if (!platform) {
|
|
191
|
+
console.error(
|
|
192
|
+
`[mink] daemon uninstall is not supported on ${process.platform} (supported: linux, darwin)`
|
|
193
|
+
);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const paths = servicePaths(platform);
|
|
198
|
+
if (!existsSync(paths.unitFile)) {
|
|
199
|
+
console.log(`[mink] no unit file at ${paths.unitFile} — nothing to uninstall`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (platform === "systemd") {
|
|
204
|
+
try {
|
|
205
|
+
execSync("systemctl --user disable --now mink-daemon.service", {
|
|
206
|
+
stdio: "ignore",
|
|
207
|
+
});
|
|
208
|
+
} catch {
|
|
209
|
+
// Service may not be enabled / running — proceed to file removal.
|
|
210
|
+
}
|
|
211
|
+
unlinkSync(paths.unitFile);
|
|
212
|
+
try {
|
|
213
|
+
execSync("systemctl --user daemon-reload", { stdio: "ignore" });
|
|
214
|
+
} catch {
|
|
215
|
+
// Ignore.
|
|
216
|
+
}
|
|
217
|
+
console.log(`[mink] removed ${paths.unitFile}`);
|
|
218
|
+
} else {
|
|
219
|
+
try {
|
|
220
|
+
execSync(`launchctl unload -w ${paths.unitFile}`, { stdio: "ignore" });
|
|
221
|
+
} catch {
|
|
222
|
+
// Ignore — may not be loaded.
|
|
223
|
+
}
|
|
224
|
+
unlinkSync(paths.unitFile);
|
|
225
|
+
console.log(`[mink] removed ${paths.unitFile}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
File without changes
|
/package/dashboard/out/_next/static/{FiL3S_40BA764FL66DRZV → EC-_8nIOf1GnPrIqZ7Mk3}/_ssgManifest.js
RENAMED
|
File without changes
|