yappr 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/.env.example +115 -0
- package/config/context/personality.md +7 -0
- package/config/context/security.md +10 -0
- package/config/hooks/example.ts +47 -0
- package/config/hooks/holder.ts +154 -0
- package/config/hooks/user-memory.ts +102 -0
- package/config/skills/compute/handler.ts +6 -0
- package/config/skills/compute/skill.md +7 -0
- package/config/skills/cron/handler.ts +89 -0
- package/config/skills/cron/skill.md +36 -0
- package/config/skills/generate-image/handler.ts +133 -0
- package/config/skills/generate-image/skill.md +20 -0
- package/config/skills/generate-meme-prompt/handler.ts +40 -0
- package/config/skills/generate-meme-prompt/skill.md +23 -0
- package/config/skills/stats/handler.ts +76 -0
- package/config/skills/stats/skill.md +18 -0
- package/config/skills/wallet/handler.ts +56 -0
- package/config/skills/wallet/skill.md +17 -0
- package/config/skills/x/handler.ts +135 -0
- package/config/skills/x/skill.md +163 -0
- package/dist/config/hooks/example.d.ts +2 -0
- package/dist/config/hooks/example.js +37 -0
- package/dist/config/hooks/holder.d.ts +2 -0
- package/dist/config/hooks/holder.js +147 -0
- package/dist/config/hooks/user-memory.d.ts +2 -0
- package/dist/config/hooks/user-memory.js +79 -0
- package/dist/config/skills/compute/handler.d.ts +2 -0
- package/dist/config/skills/compute/handler.js +5 -0
- package/dist/config/skills/cron/handler.d.ts +2 -0
- package/dist/config/skills/cron/handler.js +84 -0
- package/dist/config/skills/generate-image/handler.d.ts +2 -0
- package/dist/config/skills/generate-image/handler.js +122 -0
- package/dist/config/skills/generate-meme/handler.d.ts +2 -0
- package/dist/config/skills/generate-meme/handler.js +121 -0
- package/dist/config/skills/generate-meme-prompt/handler.d.ts +2 -0
- package/dist/config/skills/generate-meme-prompt/handler.js +38 -0
- package/dist/config/skills/stats/handler.d.ts +2 -0
- package/dist/config/skills/stats/handler.js +71 -0
- package/dist/config/skills/wallet/handler.d.ts +2 -0
- package/dist/config/skills/wallet/handler.js +54 -0
- package/dist/config/skills/x/handler.d.ts +2 -0
- package/dist/config/skills/x/handler.js +115 -0
- package/dist/src/agent-prompt.d.ts +1 -0
- package/dist/src/agent-prompt.js +45 -0
- package/dist/src/bankr.d.ts +41 -0
- package/dist/src/bankr.js +76 -0
- package/dist/src/cli/backup.d.ts +7 -0
- package/dist/src/cli/backup.js +78 -0
- package/dist/src/cli/charts.d.ts +32 -0
- package/dist/src/cli/charts.js +222 -0
- package/dist/src/cli/config-sync.d.ts +7 -0
- package/dist/src/cli/config-sync.js +71 -0
- package/dist/src/cli/deploy.d.ts +2 -0
- package/dist/src/cli/deploy.js +1059 -0
- package/dist/src/cli/env.d.ts +4 -0
- package/dist/src/cli/env.js +50 -0
- package/dist/src/cli/host-key.d.ts +4 -0
- package/dist/src/cli/host-key.js +50 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +71 -0
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +51 -0
- package/dist/src/cli/ssh.d.ts +2 -0
- package/dist/src/cli/ssh.js +141 -0
- package/dist/src/cli/status.d.ts +7 -0
- package/dist/src/cli/status.js +1184 -0
- package/dist/src/cli/tui.d.ts +18 -0
- package/dist/src/cli/tui.js +115 -0
- package/dist/src/cli/ui.d.ts +30 -0
- package/dist/src/cli/ui.js +164 -0
- package/dist/src/cli/update.d.ts +1 -0
- package/dist/src/cli/update.js +263 -0
- package/dist/src/cli/x-login.d.ts +6 -0
- package/dist/src/cli/x-login.js +70 -0
- package/dist/src/compute.d.ts +11 -0
- package/dist/src/compute.js +109 -0
- package/dist/src/config-loader.d.ts +19 -0
- package/dist/src/config-loader.js +82 -0
- package/dist/src/config.d.ts +29 -0
- package/dist/src/config.js +68 -0
- package/dist/src/cron/capability.d.ts +6 -0
- package/dist/src/cron/capability.js +66 -0
- package/dist/src/cron/runner.d.ts +2 -0
- package/dist/src/cron/runner.js +113 -0
- package/dist/src/cron/schedule.d.ts +19 -0
- package/dist/src/cron/schedule.js +154 -0
- package/dist/src/cron/store.d.ts +46 -0
- package/dist/src/cron/store.js +220 -0
- package/dist/src/db.d.ts +4 -0
- package/dist/src/db.js +53 -0
- package/dist/src/hooks/loader.d.ts +1 -0
- package/dist/src/hooks/loader.js +17 -0
- package/dist/src/hooks/registry.d.ts +17 -0
- package/dist/src/hooks/registry.js +78 -0
- package/dist/src/hooks/types.d.ts +45 -0
- package/dist/src/hooks/types.js +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.js +35 -0
- package/dist/src/llm/index.d.ts +23 -0
- package/dist/src/llm/index.js +213 -0
- package/dist/src/llm/prompts.d.ts +6 -0
- package/dist/src/llm/prompts.js +99 -0
- package/dist/src/log.d.ts +2 -0
- package/dist/src/log.js +30 -0
- package/dist/src/reply/agent.d.ts +20 -0
- package/dist/src/reply/agent.js +215 -0
- package/dist/src/reply/context-blocks.d.ts +12 -0
- package/dist/src/reply/context-blocks.js +22 -0
- package/dist/src/reply/gating.d.ts +3 -0
- package/dist/src/reply/gating.js +35 -0
- package/dist/src/reply/pipeline.d.ts +3 -0
- package/dist/src/reply/pipeline.js +144 -0
- package/dist/src/reply/poller.d.ts +5 -0
- package/dist/src/reply/poller.js +79 -0
- package/dist/src/skills/holder-access.d.ts +7 -0
- package/dist/src/skills/holder-access.js +53 -0
- package/dist/src/skills/loader.d.ts +2 -0
- package/dist/src/skills/loader.js +64 -0
- package/dist/src/skills/registry.d.ts +4 -0
- package/dist/src/skills/registry.js +10 -0
- package/dist/src/skills/types.d.ts +16 -0
- package/dist/src/skills/types.js +1 -0
- package/dist/src/state.d.ts +5 -0
- package/dist/src/state.js +26 -0
- package/dist/src/stats-cli.d.ts +1 -0
- package/dist/src/stats-cli.js +82 -0
- package/dist/src/stats.d.ts +41 -0
- package/dist/src/stats.js +236 -0
- package/dist/src/storage.d.ts +16 -0
- package/dist/src/storage.js +107 -0
- package/dist/src/treasury/abi.d.ts +99 -0
- package/dist/src/treasury/abi.js +71 -0
- package/dist/src/treasury/cycle.d.ts +16 -0
- package/dist/src/treasury/cycle.js +154 -0
- package/dist/src/treasury/index.d.ts +28 -0
- package/dist/src/treasury/index.js +222 -0
- package/dist/src/util.d.ts +3 -0
- package/dist/src/util.js +18 -0
- package/dist/src/wallet.d.ts +5 -0
- package/dist/src/wallet.js +241 -0
- package/dist/src/x/client.d.ts +74 -0
- package/dist/src/x/client.js +323 -0
- package/dist/src/x/types.d.ts +61 -0
- package/dist/src/x/types.js +1 -0
- package/dist/src/x402.d.ts +6 -0
- package/dist/src/x402.js +11 -0
- package/dist/src/yappr.d.ts +1 -0
- package/dist/src/yappr.js +85 -0
- package/package.json +52 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { input as inputPrompt, password as passwordPrompt, select as selectPrompt } from "@inquirer/prompts";
|
|
2
|
+
import { type Ora } from "ora";
|
|
3
|
+
export declare const input: typeof inputPrompt;
|
|
4
|
+
export declare const password: typeof passwordPrompt;
|
|
5
|
+
export declare function select<Value>(cfg: Parameters<typeof selectPrompt<Value>>[0], ctx?: Parameters<typeof selectPrompt<Value>>[1]): Promise<Value>;
|
|
6
|
+
export declare function confirm(message: string, defaultValue?: boolean): Promise<boolean>;
|
|
7
|
+
export declare function uiWidth(): number;
|
|
8
|
+
export declare function banner(title: string, subtitle: string): void;
|
|
9
|
+
export declare function step(n: number, total: number, label: string): void;
|
|
10
|
+
export declare function section(label: string): void;
|
|
11
|
+
export declare function kv(key: string, value: string): void;
|
|
12
|
+
export declare function printPanel(title: string, content: string[]): void;
|
|
13
|
+
export declare function ok(msg: string): void;
|
|
14
|
+
export declare function info(msg: string): void;
|
|
15
|
+
export declare function warn(msg: string): void;
|
|
16
|
+
export declare function fail(msg: string): void;
|
|
17
|
+
export declare const themeText: (s: string) => string;
|
|
18
|
+
export declare function spin<T>(label: string, fn: (spinner: Ora) => Promise<T>, doneLabel?: string): Promise<T>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Shared CLI chrome for the interactive commands (deploy, update): the logo banner,
|
|
2
|
+
// section rules, ok/info/warn/fail lines, the spinner wrapper, and inquirer prompts
|
|
3
|
+
// painted in the Base-blue palette. Keeping it here means deploy and update speak the
|
|
4
|
+
// same visual language without each re-deriving it.
|
|
5
|
+
import { input as inputPrompt, password as passwordPrompt, confirm as confirmPrompt, select as selectPrompt } from "@inquirer/prompts";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { dim, bold, green, yellow, red, accent, border, YAPPR_LOGO, kv as kvRow, fit, panel, sideBySide, centerRows, themeLine, } from "./ui.js";
|
|
8
|
+
// Inquirer renders its own prompt line (prefix + message + answer) through its theme,
|
|
9
|
+
// not our console.log — so without this it falls back to inquirer's default green "?"
|
|
10
|
+
// and the terminal's own foreground, clashing with the Base-blue palette. Paint the
|
|
11
|
+
// prefix/message/answer in the current palette so prompts match the rest of the chrome.
|
|
12
|
+
const promptTheme = {
|
|
13
|
+
prefix: { idle: accent("?"), done: green("✔") },
|
|
14
|
+
style: {
|
|
15
|
+
message: (text) => themeLine(text),
|
|
16
|
+
answer: (text) => accent(text),
|
|
17
|
+
// dim() only adds the dim attribute (no color) → the "(Y/n)" hint would fall back
|
|
18
|
+
// to the terminal's own foreground (green). Layer dim over the palette color.
|
|
19
|
+
defaultAnswer: (text) => dim(accent(text)),
|
|
20
|
+
highlight: (text) => accent(text),
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const withTheme = (cfg) => ({ ...cfg, theme: { ...promptTheme, ...(cfg.theme ?? {}) } });
|
|
24
|
+
export const input = (cfg, ctx) => inputPrompt(withTheme(cfg), ctx);
|
|
25
|
+
export const password = (cfg, ctx) => passwordPrompt(withTheme(cfg), ctx);
|
|
26
|
+
const inquirerConfirm = (cfg, ctx) => confirmPrompt(withTheme(cfg), ctx);
|
|
27
|
+
export function select(cfg, ctx) {
|
|
28
|
+
return selectPrompt(withTheme(cfg), ctx);
|
|
29
|
+
}
|
|
30
|
+
// `confirm` from inquirer, wrapped so Ctrl-C exits cleanly instead of throwing.
|
|
31
|
+
export async function confirm(message, defaultValue = false) {
|
|
32
|
+
return inquirerConfirm({ message, default: defaultValue });
|
|
33
|
+
}
|
|
34
|
+
// Panel width for the deploy/update chrome — match the terminal, capped so the boxes
|
|
35
|
+
// stay readable on very wide windows.
|
|
36
|
+
export function uiWidth() {
|
|
37
|
+
return Math.max(48, Math.min((process.stdout.columns ?? 80) - 1, 78));
|
|
38
|
+
}
|
|
39
|
+
// Header: the bare logo art with the command title floating beside it, vertically
|
|
40
|
+
// centred. `title` is the command (e.g. "Deploy", "Update").
|
|
41
|
+
export function banner(title, subtitle) {
|
|
42
|
+
const logoW = 17; // raw logo art width (no box)
|
|
43
|
+
const h = YAPPR_LOGO.length;
|
|
44
|
+
const info = centerRows([
|
|
45
|
+
`${bold("YAPPR")} ${dim("—")} ${bold(title)}`,
|
|
46
|
+
dim(subtitle),
|
|
47
|
+
], h).map((line) => ` ${line}`);
|
|
48
|
+
console.log("");
|
|
49
|
+
// fit() each logo row to a fixed width so the text column lines up exactly.
|
|
50
|
+
for (const row of sideBySide(YAPPR_LOGO.map((l) => " " + fit(l, logoW)), logoW + 2, info, 0)) {
|
|
51
|
+
console.log(row);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Numbered step header (deploy), styled like a dashboard panel title: bold caps in a
|
|
55
|
+
// Base-blue rule with a "step n/total" counter.
|
|
56
|
+
export function step(n, total, label) {
|
|
57
|
+
const name = label.toUpperCase();
|
|
58
|
+
const counter = `step ${n}/${total}`;
|
|
59
|
+
const fill = Math.max(2, uiWidth() - name.length - counter.length - 10);
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(` ${border("──")} ${bold(name)} ${border("─".repeat(fill))} ${dim(counter)} ${border("──")}`);
|
|
62
|
+
}
|
|
63
|
+
// Section header (update): the same rule as step() but without a counter — for flows
|
|
64
|
+
// that have phases rather than numbered steps.
|
|
65
|
+
export function section(label) {
|
|
66
|
+
const name = label.toUpperCase();
|
|
67
|
+
const fill = Math.max(2, uiWidth() - name.length - 8);
|
|
68
|
+
console.log("");
|
|
69
|
+
console.log(` ${border("──")} ${bold(name)} ${border("─".repeat(fill))} ${border("──")}`);
|
|
70
|
+
}
|
|
71
|
+
// Aligned dim-label key/value row (the shared kv style from the status dashboard).
|
|
72
|
+
export function kv(key, value) {
|
|
73
|
+
console.log(` ${kvRow(key, value)}`);
|
|
74
|
+
}
|
|
75
|
+
// Print a status-style bordered panel at the 2-space indent the flows use.
|
|
76
|
+
export function printPanel(title, content) {
|
|
77
|
+
for (const line of panel(title, content, uiWidth() - 2))
|
|
78
|
+
console.log(` ${line}`);
|
|
79
|
+
}
|
|
80
|
+
export function ok(msg) { console.log(` ${green("✓")} ${msg}`); }
|
|
81
|
+
export function info(msg) { console.log(` ${dim(msg)}`); }
|
|
82
|
+
export function warn(msg) { console.log(` ${yellow("⚠")} ${yellow(msg)}`); }
|
|
83
|
+
export function fail(msg) { console.log(` ${red("✗")} ${red(msg)}`); }
|
|
84
|
+
// ora's clear() parks the cursor at the `indent` column, so a following console.log
|
|
85
|
+
// would inherit those spaces. Reset to column 0 first so spinner result lines line up
|
|
86
|
+
// exactly with ok()/fail() lines.
|
|
87
|
+
function stopSpinner(spinner) {
|
|
88
|
+
spinner.stop();
|
|
89
|
+
if (process.stdout.isTTY)
|
|
90
|
+
process.stdout.cursorTo(0);
|
|
91
|
+
}
|
|
92
|
+
// ora draws its frame straight to the stream (not via our themed console.log), so its
|
|
93
|
+
// label text would render in the terminal's own foreground (green). themeText paints
|
|
94
|
+
// it in the palette; stripAnsi recovers the raw label for the static done line.
|
|
95
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
96
|
+
// Paint a (possibly already-colored) string in the palette — exported so callers can
|
|
97
|
+
// re-theme a spinner's live `.text` mid-run (ora bypasses our themed console.log).
|
|
98
|
+
export const themeText = (s) => themeLine(stripAnsi(s));
|
|
99
|
+
// Run an async task behind a spinner, then resolve to a static line that uses the same
|
|
100
|
+
// ✓/✗ glyphs and spacing as ok()/fail() so everything stays aligned.
|
|
101
|
+
export async function spin(label, fn, doneLabel) {
|
|
102
|
+
const spinner = ora({ text: themeText(label), indent: 2 }).start();
|
|
103
|
+
try {
|
|
104
|
+
const result = await fn(spinner);
|
|
105
|
+
const text = stripAnsi(spinner.text);
|
|
106
|
+
stopSpinner(spinner);
|
|
107
|
+
ok(doneLabel ?? text);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
stopSpinner(spinner);
|
|
112
|
+
fail(label);
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type ThemeName = "dark" | "light";
|
|
2
|
+
export declare const themeName: () => ThemeName;
|
|
3
|
+
export declare function setTheme(name: ThemeName): void;
|
|
4
|
+
export declare function toggleTheme(): ThemeName;
|
|
5
|
+
export declare const chartRgb: () => {
|
|
6
|
+
spent: string;
|
|
7
|
+
earn: string;
|
|
8
|
+
xapi: string;
|
|
9
|
+
inference: string;
|
|
10
|
+
compute: string;
|
|
11
|
+
x402: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function detectTerminalTheme(timeoutMs?: number): Promise<ThemeName | null>;
|
|
14
|
+
export declare const dim: (s: string) => string;
|
|
15
|
+
export declare const bold: (s: string) => string;
|
|
16
|
+
export declare const green: (s: string) => string;
|
|
17
|
+
export declare const yellow: (s: string) => string;
|
|
18
|
+
export declare const red: (s: string) => string;
|
|
19
|
+
export declare const cyan: (s: string) => string;
|
|
20
|
+
export declare const accent: (s: string) => string;
|
|
21
|
+
export declare const border: (s: string) => string;
|
|
22
|
+
export declare const themeLine: (l: string) => string;
|
|
23
|
+
export declare function fit(s: string, width: number): string;
|
|
24
|
+
export declare const kv: (label: string, value: string, pad?: number) => string;
|
|
25
|
+
export declare function panel(title: string, content: string[], width: number): string[];
|
|
26
|
+
export declare function sideBySide(a: string[], aw: number, b: string[], bw: number, gap?: number): string[];
|
|
27
|
+
export declare const padRows: (lines: string[], n: number) => any[];
|
|
28
|
+
export declare const centerRows: (lines: string[], n: number) => any[];
|
|
29
|
+
export declare const YAPPR_ART: string[];
|
|
30
|
+
export declare const YAPPR_LOGO: string[];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Shared terminal styling + box/layout primitives for the CLI scripts (deploy,
|
|
2
|
+
// status), so both speak the same visual language: dim-label kv rows and
|
|
3
|
+
// rounded-border panels with the title in the top edge.
|
|
4
|
+
import stringWidth from "string-width";
|
|
5
|
+
import cliTruncate from "cli-truncate";
|
|
6
|
+
const THEMES = {
|
|
7
|
+
dark: {
|
|
8
|
+
text: "143;191;255", green: "77;139;255", yellow: "102;163;255", red: "0;82;255",
|
|
9
|
+
cyan: "143;191;255", accent: "51;116;255", border: "0;82;255",
|
|
10
|
+
chart: { spent: "0;82;255", earn: "143;191;255", xapi: "143;191;255", inference: "77;139;255", compute: "38;99;255", x402: "170;120;255" },
|
|
11
|
+
},
|
|
12
|
+
light: {
|
|
13
|
+
text: "30;58;138", green: "0;82;255", yellow: "59;130;246", red: "30;64;175",
|
|
14
|
+
cyan: "37;99;235", accent: "29;78;216", border: "0;82;255",
|
|
15
|
+
chart: { spent: "30;58;138", earn: "59;130;246", xapi: "59;130;246", inference: "0;82;255", compute: "147;180;255", x402: "124;58;237" },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
let _name = process.env.STATUS_THEME === "light" ? "light" : "dark";
|
|
19
|
+
let _p = THEMES[_name];
|
|
20
|
+
export const themeName = () => _name;
|
|
21
|
+
export function setTheme(name) { _name = name; _p = THEMES[name]; }
|
|
22
|
+
export function toggleTheme() { setTheme(_name === "dark" ? "light" : "dark"); return _name; }
|
|
23
|
+
// Chart RGB triples for the current theme (charts need raw triples, not helpers,
|
|
24
|
+
// for the half-block fg+bg cells).
|
|
25
|
+
export const chartRgb = () => _p.chart;
|
|
26
|
+
// COLORFGBG fallback ("15;0" = light fg on dark bg): last field is the bg's
|
|
27
|
+
// 16-color index — 7/15 are light backgrounds. Set by rxvt/konsole/some iTerm.
|
|
28
|
+
function themeFromColorFgBg() {
|
|
29
|
+
const parts = (process.env.COLORFGBG ?? "").split(";");
|
|
30
|
+
const bg = Number(parts[parts.length - 1]);
|
|
31
|
+
if (!Number.isFinite(bg) || parts.length < 2)
|
|
32
|
+
return null;
|
|
33
|
+
return bg === 7 || bg === 15 ? "light" : "dark";
|
|
34
|
+
}
|
|
35
|
+
// Ask the terminal for its background color (OSC 11 query; answered by iTerm2,
|
|
36
|
+
// Terminal.app, kitty, alacritty, wezterm, …) and classify by luminance. Falls
|
|
37
|
+
// back to COLORFGBG, then null (caller keeps the default) when the terminal
|
|
38
|
+
// stays silent past the timeout. Briefly takes stdin raw to read the reply —
|
|
39
|
+
// run BEFORE the dashboard's own key handling is attached.
|
|
40
|
+
export function detectTerminalTheme(timeoutMs = 250) {
|
|
41
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY)
|
|
42
|
+
return Promise.resolve(themeFromColorFgBg());
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const stdin = process.stdin;
|
|
45
|
+
let buf = "";
|
|
46
|
+
const done = (v) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
stdin.removeListener("data", onData);
|
|
49
|
+
try {
|
|
50
|
+
stdin.setRawMode(false);
|
|
51
|
+
}
|
|
52
|
+
catch { /* ignore */ }
|
|
53
|
+
stdin.pause();
|
|
54
|
+
resolve(v);
|
|
55
|
+
};
|
|
56
|
+
const onData = (d) => {
|
|
57
|
+
buf += d.toString("utf8");
|
|
58
|
+
// Reply: ESC ] 11 ; rgb:RRRR/GGGG/BBBB (components are 1-4 hex digits per
|
|
59
|
+
// channel; the leading 2 digits carry the top 8 bits we care about).
|
|
60
|
+
const m = buf.match(/\]11;rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
|
|
61
|
+
if (!m)
|
|
62
|
+
return;
|
|
63
|
+
const [r, g, b] = [m[1], m[2], m[3]].map((s) => parseInt(s.padEnd(2, s).slice(0, 2), 16) / 255);
|
|
64
|
+
const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
65
|
+
done(luminance > 0.5 ? "light" : "dark");
|
|
66
|
+
};
|
|
67
|
+
const timer = setTimeout(() => done(themeFromColorFgBg()), timeoutMs);
|
|
68
|
+
try {
|
|
69
|
+
stdin.setRawMode(true);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
done(themeFromColorFgBg());
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
stdin.resume();
|
|
76
|
+
stdin.on("data", onData);
|
|
77
|
+
process.stdout.write("\x1b]11;?\x1b\\");
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
export const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
81
|
+
export const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
82
|
+
export const green = (s) => `\x1b[38;2;${_p.green}m${s}\x1b[0m`;
|
|
83
|
+
export const yellow = (s) => `\x1b[38;2;${_p.yellow}m${s}\x1b[0m`;
|
|
84
|
+
export const red = (s) => `\x1b[38;2;${_p.red}m${s}\x1b[0m`;
|
|
85
|
+
export const cyan = (s) => `\x1b[38;2;${_p.cyan}m${s}\x1b[0m`;
|
|
86
|
+
export const accent = (s) => `\x1b[38;2;${_p.accent}m${s}\x1b[0m`;
|
|
87
|
+
export const border = (s) => `\x1b[38;2;${_p.border}m${s}\x1b[0m`; // box borders — Base blue (#0052FF)
|
|
88
|
+
// Default color for otherwise-uncolored text. Without this, plain text renders
|
|
89
|
+
// in the terminal theme's default foreground (green, white, whatever the user
|
|
90
|
+
// has) — applying it at render time keeps the whole frame on-palette. Re-armed
|
|
91
|
+
// after every reset so it acts as the baseline without overriding explicit colors.
|
|
92
|
+
export const themeLine = (l) => {
|
|
93
|
+
const t = `\x1b[38;2;${_p.text}m`;
|
|
94
|
+
return t + l.replaceAll("\x1b[0m", "\x1b[0m" + t) + "\x1b[0m";
|
|
95
|
+
};
|
|
96
|
+
// ─── layout primitives (ANSI- and wide-char-aware via string-width) ────────────
|
|
97
|
+
// Fit a (possibly colored) string to an exact display width: pad with spaces or
|
|
98
|
+
// truncate with an ellipsis, preserving ANSI codes.
|
|
99
|
+
export function fit(s, width) {
|
|
100
|
+
const w = stringWidth(s);
|
|
101
|
+
if (w === width)
|
|
102
|
+
return s;
|
|
103
|
+
if (w < width)
|
|
104
|
+
return s + " ".repeat(width - w);
|
|
105
|
+
return cliTruncate(s, width, { position: "end", truncationCharacter: "~" });
|
|
106
|
+
}
|
|
107
|
+
// A labelled value row, with the label dimmed and padded for column alignment.
|
|
108
|
+
export const kv = (label, value, pad = 9) => dim(label.padEnd(pad)) + value;
|
|
109
|
+
// Render a rounded-border panel of a fixed total width. Title sits in the top edge.
|
|
110
|
+
export function panel(title, content, width) {
|
|
111
|
+
const inner = width - 4; // "│ " + content + " │"
|
|
112
|
+
const fillLen = Math.max(0, width - 5 - stringWidth(title)); // ╭ ─ " title " ─*fill ╮ (ANSI-aware)
|
|
113
|
+
const top = border("┌─") + bold(` ${title} `) + border("─".repeat(fillLen) + "┐");
|
|
114
|
+
const bottom = border("└" + "─".repeat(width - 2) + "┘");
|
|
115
|
+
const body = content.map((line) => border("│") + " " + fit(line, inner) + "\x1b[0m " + border("│"));
|
|
116
|
+
return [top, ...body, bottom];
|
|
117
|
+
}
|
|
118
|
+
// Lay two equal-or-fixed-width panels next to each other.
|
|
119
|
+
export function sideBySide(a, aw, b, bw, gap = 1) {
|
|
120
|
+
const h = Math.max(a.length, b.length);
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (let i = 0; i < h; i++) {
|
|
123
|
+
rows.push((a[i] ?? " ".repeat(aw)) + " ".repeat(gap) + (b[i] ?? " ".repeat(bw)));
|
|
124
|
+
}
|
|
125
|
+
return rows;
|
|
126
|
+
}
|
|
127
|
+
// Pad a content array with blank rows so stacked panels share one height.
|
|
128
|
+
export const padRows = (lines, n) => (lines.length >= n ? lines : [...lines, ...Array(n - lines.length).fill("")]);
|
|
129
|
+
// Like padRows, but split the padding above/below so shorter info panels sit
|
|
130
|
+
// vertically centred next to the taller logo panel.
|
|
131
|
+
export const centerRows = (lines, n) => {
|
|
132
|
+
if (lines.length >= n)
|
|
133
|
+
return lines;
|
|
134
|
+
const top = Math.floor((n - lines.length) / 2);
|
|
135
|
+
return [...Array(top).fill(""), ...lines, ...Array(n - lines.length - top).fill("")];
|
|
136
|
+
};
|
|
137
|
+
export const YAPPR_ART = [
|
|
138
|
+
"██╗ ██╗ █████╗ ██████╗ ██████╗ ██████╗ ",
|
|
139
|
+
"╚██╗ ██╔╝██╔══██╗██╔══██╗██╔══██╗██╔══██╗",
|
|
140
|
+
" ╚████╔╝ ███████║██████╔╝██████╔╝██████╔╝",
|
|
141
|
+
" ╚██╔╝ ██╔══██║██╔═══╝ ██╔═══╝ ██╔══██╗",
|
|
142
|
+
" ██║ ██║ ██║██║ ██║ ██║ ██║ ",
|
|
143
|
+
" ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ",
|
|
144
|
+
];
|
|
145
|
+
// Brand logo (quadrant art, 17x9 cells) from yappr-8.png — flat #0ff991 green face with
|
|
146
|
+
// solid black eyes + mouth (cleaned at 2x then downsampled so the mouth is a solid
|
|
147
|
+
// blob, no tongue). Transparent bg renders as spaces.
|
|
148
|
+
const RAW_LOGO = [
|
|
149
|
+
" \u001b[38;2;0;82;255m▟\u001b[0m\u001b[38;2;0;82;255m▘\u001b[0m\u001b[38;2;0;82;255m▗\u001b[0m\u001b[38;2;0;82;255m▄\u001b[0m",
|
|
150
|
+
" \u001b[38;2;0;82;255m▄\u001b[0m\u001b[38;2;0;82;255m▟\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m▄\u001b[0m\u001b[38;2;0;82;255m▖\u001b[0m\u001b[38;2;0;82;255m▝\u001b[0m\u001b[38;2;0;82;255m▜\u001b[0m\u001b[38;2;0;82;255m▖\u001b[0m",
|
|
151
|
+
" \u001b[38;2;0;82;255m▄\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▗\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▖\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▀\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m▙\u001b[0m ",
|
|
152
|
+
"\u001b[38;2;0;82;255m▟\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▟\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▙\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m▙\u001b[0m ",
|
|
153
|
+
"\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▟\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m ",
|
|
154
|
+
"\u001b[38;2;0;82;255m▜\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▜\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▛\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m▛\u001b[0m ",
|
|
155
|
+
" \u001b[38;2;0;82;255m▜\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▀\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0m█\u001b[0m\u001b[38;2;0;0;0;48;2;0;82;255m▀\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m▛\u001b[0m ",
|
|
156
|
+
" \u001b[38;2;0;82;255m▝\u001b[0m\u001b[38;2;0;82;255m▜\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m▛\u001b[0m\u001b[38;2;0;82;255m▘\u001b[0m ",
|
|
157
|
+
" \u001b[38;2;0;82;255m▐\u001b[0m\u001b[38;2;0;82;255m█\u001b[0m\u001b[38;2;0;82;255m▀\u001b[0m\u001b[38;2;0;82;255m▀\u001b[0m\u001b[38;2;0;82;255m▀\u001b[0m ",
|
|
158
|
+
];
|
|
159
|
+
// Full-block cells (fg-only `█`) only paint the font's em-box, so terminals with
|
|
160
|
+
// extra line spacing show the terminal background between rows — invisible on a
|
|
161
|
+
// dark background, white stripes on a light one. Cell BACKGROUNDS do fill the
|
|
162
|
+
// whole line height, so solid cells render as a bg-painted space instead. Edge
|
|
163
|
+
// quadrant glyphs keep their fg form (their empty half must stay transparent).
|
|
164
|
+
export const YAPPR_LOGO = RAW_LOGO.map((l) => l.replaceAll(/\x1b\[38;2;(\d+;\d+;\d+)m█\x1b\[0m/g, "\x1b[48;2;$1m \x1b[0m"));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(): Promise<void>;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// `yappr update` — pull the latest engine + scaffolded skills/hooks/context into an
|
|
2
|
+
// instance, then offer to redeploy. Two phases (a TUI like deploy, but no numbered
|
|
3
|
+
// steps): (1) bump the package to yappr@latest, (2) reconcile the new config/ into the
|
|
4
|
+
// user's ./config with the rules below. Run it from the instance dir.
|
|
5
|
+
//
|
|
6
|
+
// Reconcile rules, per shipped file (skill/hook/context):
|
|
7
|
+
// • the user hasn't touched it → overwrite silently with the new version
|
|
8
|
+
// • the user edited it → ask: keep mine / replace with new / show diff
|
|
9
|
+
// • brand-new file → create it; if a same-name file already exists
|
|
10
|
+
// (the user made their own), ask before overwriting
|
|
11
|
+
// "Untouched vs edited" is decided against config/.yappr-sync.json, the manifest init
|
|
12
|
+
// writes (and this command rewrites). Instances with no manifest fall back to treating
|
|
13
|
+
// every existing file as edited → ask, so we never silently clobber.
|
|
14
|
+
import { resolve, join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
|
+
import { promisify } from "node:util";
|
|
20
|
+
import { banner, section, spin, ok, info, warn, fail, kv, printPanel, select, confirm, } from "./tui.js";
|
|
21
|
+
import { dim, bold, green, yellow, red, accent, setTheme, detectTerminalTheme, themeLine } from "./ui.js";
|
|
22
|
+
import { relFiles, hashFile, loadManifest, saveManifest, } from "./config-sync.js";
|
|
23
|
+
const execFileAsync = promisify(execFile);
|
|
24
|
+
// This file is <pkg>/dist/src/cli/update.js (prod) or <pkg>/src/cli/update.ts (dev),
|
|
25
|
+
// so the package root — and its shipped config/ template — is three levels up. After
|
|
26
|
+
// the install phase replaces the package on disk, this same path holds the new config.
|
|
27
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
28
|
+
function parseFlags(argv) {
|
|
29
|
+
const has = (...names) => names.some((n) => argv.includes(n));
|
|
30
|
+
return {
|
|
31
|
+
configOnly: has("--config-only", "--no-install"),
|
|
32
|
+
yes: has("--yes", "-y"),
|
|
33
|
+
theirs: has("--theirs", "--force"),
|
|
34
|
+
ours: has("--ours", "--keep"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
// Which package manager runs the instance — pick by lockfile, default npm. Each is the
|
|
38
|
+
// "install this exact dist-tag" form so `yappr@latest` lands.
|
|
39
|
+
function installCmd(cwd) {
|
|
40
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")))
|
|
41
|
+
return { cmd: "pnpm", args: ["add", "yappr@latest"] };
|
|
42
|
+
if (existsSync(join(cwd, "yarn.lock")))
|
|
43
|
+
return { cmd: "yarn", args: ["add", "yappr@latest"] };
|
|
44
|
+
if (existsSync(join(cwd, "bun.lockb")))
|
|
45
|
+
return { cmd: "bun", args: ["add", "yappr@latest"] };
|
|
46
|
+
return { cmd: "npm", args: ["install", "yappr@latest"] };
|
|
47
|
+
}
|
|
48
|
+
// Print a coloured unified diff of the user's file vs the new one (best-effort: relies
|
|
49
|
+
// on the system `diff`, which exits 1 when files differ — we read that as the diff).
|
|
50
|
+
async function showDiff(userPath, pkgPath) {
|
|
51
|
+
try {
|
|
52
|
+
await execFileAsync("diff", ["-u", userPath, pkgPath]);
|
|
53
|
+
info("(no differences)");
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const out = typeof err?.stdout === "string" ? err.stdout : "";
|
|
57
|
+
if (!out) {
|
|
58
|
+
info("(diff unavailable)");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const line of out.split("\n")) {
|
|
62
|
+
if (line.startsWith("+") && !line.startsWith("+++"))
|
|
63
|
+
console.log(` ${green(line)}`);
|
|
64
|
+
else if (line.startsWith("-") && !line.startsWith("---"))
|
|
65
|
+
console.log(` ${red(line)}`);
|
|
66
|
+
else if (line.startsWith("@@"))
|
|
67
|
+
console.log(` ${accent(line)}`);
|
|
68
|
+
else
|
|
69
|
+
console.log(` ${dim(line)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Resolve a conflict (user-edited or a name-clash on a new file): keep theirs, take
|
|
74
|
+
// ours, or — interactively — let them choose (with an optional diff).
|
|
75
|
+
async function resolveConflict(rel, userPath, pkgPath, flags) {
|
|
76
|
+
if (flags.theirs)
|
|
77
|
+
return "replace";
|
|
78
|
+
if (flags.ours || flags.yes || !process.stdout.isTTY)
|
|
79
|
+
return "keep"; // non-interactive default: never clobber edits
|
|
80
|
+
for (;;) {
|
|
81
|
+
const choice = await select({
|
|
82
|
+
message: `${accent(rel)} differs from the new version — what should I do?`,
|
|
83
|
+
choices: [
|
|
84
|
+
{ name: "Keep my version", value: "keep" },
|
|
85
|
+
{ name: "Replace with the new version", value: "replace" },
|
|
86
|
+
{ name: "Show diff", value: "diff" },
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
if (choice === "diff") {
|
|
90
|
+
await showDiff(userPath, pkgPath);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
return choice;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function reconcile(configDir, templateDir, flags) {
|
|
97
|
+
const counts = { create: 0, update: 0, overwrite: 0, keep: 0, current: 0 };
|
|
98
|
+
const shipped = await relFiles(templateDir);
|
|
99
|
+
const manifest = await loadManifest(configDir);
|
|
100
|
+
const next = {};
|
|
101
|
+
for (const rel of shipped) {
|
|
102
|
+
const userPath = join(configDir, rel);
|
|
103
|
+
const pkgPath = join(templateDir, rel);
|
|
104
|
+
const [userHash, newHash] = await Promise.all([hashFile(userPath), hashFile(pkgPath)]);
|
|
105
|
+
if (!newHash)
|
|
106
|
+
continue; // unreadable shipped file — skip defensively
|
|
107
|
+
const recorded = manifest[rel];
|
|
108
|
+
const write = async () => {
|
|
109
|
+
await mkdir(dirname(userPath), { recursive: true });
|
|
110
|
+
await writeFile(userPath, await readFile(pkgPath));
|
|
111
|
+
next[rel] = newHash;
|
|
112
|
+
};
|
|
113
|
+
if (userHash === null) {
|
|
114
|
+
// Not present in the instance → a brand-new file. Create it.
|
|
115
|
+
await write();
|
|
116
|
+
counts.create++;
|
|
117
|
+
ok(`added ${accent(rel)}`);
|
|
118
|
+
}
|
|
119
|
+
else if (userHash === newHash) {
|
|
120
|
+
// Already identical to the new version — nothing to do.
|
|
121
|
+
next[rel] = newHash;
|
|
122
|
+
counts.current++;
|
|
123
|
+
}
|
|
124
|
+
else if (recorded && userHash === recorded) {
|
|
125
|
+
// Untouched by the user since we last installed it, but the package changed →
|
|
126
|
+
// safe to fast-forward.
|
|
127
|
+
await write();
|
|
128
|
+
counts.update++;
|
|
129
|
+
ok(`updated ${accent(rel)}`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Diverged: the user edited it, or it pre-existed untracked (their own file with
|
|
133
|
+
// the same name). Never clobber without consent.
|
|
134
|
+
const decision = await resolveConflict(rel, userPath, pkgPath, flags);
|
|
135
|
+
if (decision === "replace") {
|
|
136
|
+
await write();
|
|
137
|
+
counts.overwrite++;
|
|
138
|
+
ok(`replaced ${accent(rel)} ${dim("(your version backed out)")}`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
next[rel] = recorded ?? userHash; // keep detecting against the same baseline
|
|
142
|
+
counts.keep++;
|
|
143
|
+
info(`kept your ${rel}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await saveManifest(configDir, next);
|
|
148
|
+
return counts;
|
|
149
|
+
}
|
|
150
|
+
async function main() {
|
|
151
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
152
|
+
const cwd = process.cwd();
|
|
153
|
+
const configDir = join(cwd, "config");
|
|
154
|
+
if (process.stdout.isTTY)
|
|
155
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
156
|
+
if (!process.env.STATUS_THEME) {
|
|
157
|
+
const detected = await detectTerminalTheme().catch(() => null);
|
|
158
|
+
if (detected)
|
|
159
|
+
setTheme(detected);
|
|
160
|
+
}
|
|
161
|
+
// Paint every printed line in the palette's default text color (see deploy.ts /
|
|
162
|
+
// ui.themeLine): the chrome only colors specific glyphs and leaves message text
|
|
163
|
+
// uncolored, which otherwise falls back to the terminal's own foreground.
|
|
164
|
+
if (process.stdout.isTTY) {
|
|
165
|
+
for (const m of ["log", "error"]) {
|
|
166
|
+
const orig = console[m].bind(console);
|
|
167
|
+
console[m] = (...args) => orig(...args.map((a) => (typeof a === "string" ? themeLine(a) : a)));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
banner("Update", "Pull the latest engine + skills into this instance");
|
|
171
|
+
if (!existsSync(configDir)) {
|
|
172
|
+
console.log("");
|
|
173
|
+
fail(`No ./config here — run \`yappr update\` from your instance directory (where you ran \`yappr init\`).`);
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// ── Phase 1: bump the package ────────────────────────────────────────────────
|
|
178
|
+
// Only when yappr is actually an installed dependency of this instance (skip in dev
|
|
179
|
+
// / when running the engine in place, where there's nothing to install).
|
|
180
|
+
const installedHere = existsSync(join(cwd, "node_modules", "yappr"));
|
|
181
|
+
section("Package");
|
|
182
|
+
if (flags.configOnly) {
|
|
183
|
+
info("--config-only — skipping package update");
|
|
184
|
+
}
|
|
185
|
+
else if (!installedHere) {
|
|
186
|
+
warn("yappr isn't an installed dependency here — skipping package update (syncing config only)");
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
const { cmd, args } = installCmd(cwd);
|
|
190
|
+
try {
|
|
191
|
+
await spin(`Updating engine — ${dim(`${cmd} ${args.join(" ")}`)}…`, () => execFileAsync(cmd, args, { cwd }), "Engine updated to yappr@latest");
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
fail(`Package update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
195
|
+
if (!(await confirm("Continue and just reconcile config from the currently-installed version?", true))) {
|
|
196
|
+
process.exitCode = 1;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// ── Phase 2: reconcile config ────────────────────────────────────────────────
|
|
202
|
+
const templateDir = join(PKG_ROOT, "config");
|
|
203
|
+
section("Config");
|
|
204
|
+
if (!existsSync(templateDir)) {
|
|
205
|
+
fail(`Installed package has no config/ template at ${templateDir}.`);
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const c = await reconcile(configDir, templateDir, flags);
|
|
210
|
+
const touched = c.create + c.update + c.overwrite;
|
|
211
|
+
if (touched === 0 && c.keep === 0)
|
|
212
|
+
info("everything already up to date");
|
|
213
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
214
|
+
console.log("");
|
|
215
|
+
printPanel("UPDATE SUMMARY", [
|
|
216
|
+
kvFmt("Added", c.create, green),
|
|
217
|
+
kvFmt("Updated", c.update, green),
|
|
218
|
+
kvFmt("Replaced", c.overwrite, yellow),
|
|
219
|
+
kvFmt("Kept yours", c.keep, dim),
|
|
220
|
+
kvFmt("Unchanged", c.current, dim),
|
|
221
|
+
]);
|
|
222
|
+
// ── Next: redeploy ─────────────────────────────────────────────────────────────
|
|
223
|
+
section("Next");
|
|
224
|
+
if (touched === 0) {
|
|
225
|
+
info("No config changes — nothing to redeploy.");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
console.log(` ${yellow("⚠")} ${bold("Your live agent is still running the previous version.")}`);
|
|
229
|
+
info("These changes only take effect on the server after a redeploy.");
|
|
230
|
+
console.log("");
|
|
231
|
+
if (process.stdout.isTTY && await confirm("Redeploy now to apply the update?", true)) {
|
|
232
|
+
const { run } = await import("./deploy.js");
|
|
233
|
+
await run();
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
kv("Redeploy", `${bold("npx yappr deploy")} ${dim("— push these changes to your instance")}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// A right-aligned-ish count row for the summary panel, coloured by the formatter.
|
|
240
|
+
function kvFmt(label, n, color) {
|
|
241
|
+
return `${dim(label.padEnd(11))}${color(String(n))}`;
|
|
242
|
+
}
|
|
243
|
+
export async function run() {
|
|
244
|
+
await main();
|
|
245
|
+
}
|
|
246
|
+
const isMain = (() => {
|
|
247
|
+
try {
|
|
248
|
+
return fileURLToPath(import.meta.url) === resolve(process.argv[1] ?? "");
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
})();
|
|
254
|
+
if (isMain) {
|
|
255
|
+
run().catch((err) => {
|
|
256
|
+
if (err?.name === "ExitPromptError") {
|
|
257
|
+
console.log("\n Aborted.");
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
console.error(`\n ✗ ${err?.message ?? err}`);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
});
|
|
263
|
+
}
|