lazy-skill 0.3.0 → 0.4.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/dist/commands/connect.js +12 -8
- package/dist/commands/install.js +2 -2
- package/dist/commands/skills.js +2 -2
- package/dist/commands/status.js +2 -2
- package/dist/commands/theme.js +28 -13
- package/dist/index.js +10 -2
- package/dist/lib/config.js +4 -0
- package/dist/ui/layout.js +41 -29
- package/dist/ui/onboarding.js +47 -0
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -3,8 +3,9 @@ import { api, ApiError } from "../lib/api.js";
|
|
|
3
3
|
import { readConfig, updateConfig } from "../lib/config.js";
|
|
4
4
|
import { detectAgents } from "../adapters/index.js";
|
|
5
5
|
import { THEMES, style } from "../ui/theme.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { boxWidth, visibleWidth } from "../ui/layout.js";
|
|
7
|
+
import { renderQr } from "../ui/qr.js";
|
|
8
|
+
import { blank, box, centre, fail, hint, line, muted, ok, status, welcome } from "../ui/layout.js";
|
|
8
9
|
import { messages } from "../ui/messages.js";
|
|
9
10
|
import { Spinner } from "../ui/spinner.js";
|
|
10
11
|
function friendlyPlatform() {
|
|
@@ -23,8 +24,9 @@ export async function connectCommand(options) {
|
|
|
23
24
|
const config = readConfig();
|
|
24
25
|
const theme = THEMES[config.theme];
|
|
25
26
|
blank();
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
welcome(theme, "Welcome to Lazy Skill!");
|
|
28
|
+
blank();
|
|
29
|
+
hint("See it. Search it. Install it.");
|
|
28
30
|
blank();
|
|
29
31
|
// Detection runs before anything is drawn: a spinner writes to the current
|
|
30
32
|
// line, and anything transient printed mid-layout tears it.
|
|
@@ -59,11 +61,13 @@ export async function connectCommand(options) {
|
|
|
59
61
|
blank();
|
|
60
62
|
return 1;
|
|
61
63
|
}
|
|
62
|
-
|
|
63
|
-
muted("Open Lazy Skill on your phone and point the camera here.");
|
|
64
|
+
hint("Open Lazy Skill on your phone and scan this.");
|
|
64
65
|
blank();
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
// The QR is the one thing to act on, so it gets the box. Its own quiet zone
|
|
67
|
+
// sits inside the border, untouched.
|
|
68
|
+
const plate = await renderQr(session.pairUrl);
|
|
69
|
+
const width = plate.length ? visibleWidth(plate[0]) + 4 : boxWidth();
|
|
70
|
+
box(theme, plate.map((row) => centre(row, width)), width);
|
|
67
71
|
blank();
|
|
68
72
|
muted(session.pairUrl);
|
|
69
73
|
blank();
|
package/dist/commands/install.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
2
2
|
import { readConfig } from "../lib/config.js";
|
|
3
3
|
import { adapterFor, createInstallPlan, describePlan, detectAgents, InvalidPlanError, } from "../adapters/index.js";
|
|
4
4
|
import { THEMES, rgb, style } from "../ui/theme.js";
|
|
5
|
-
import { blank,
|
|
5
|
+
import { blank, fail, line, muted, ok, status, welcome } from "../ui/layout.js";
|
|
6
6
|
import { messages } from "../ui/messages.js";
|
|
7
7
|
const STAGE_LABEL = {
|
|
8
8
|
starting: "Preparing",
|
|
@@ -87,7 +87,7 @@ export async function installCommand(ref, options = { yes: false }) {
|
|
|
87
87
|
}
|
|
88
88
|
// Show exactly what will run, before it runs.
|
|
89
89
|
blank();
|
|
90
|
-
|
|
90
|
+
welcome(theme, "Lazy Skill");
|
|
91
91
|
blank();
|
|
92
92
|
line(`${style.bold(ref)}`);
|
|
93
93
|
muted(`for ${targets.map((t) => t.label).join(", ")} · ${options.project ? "this project" : "global"}`);
|
package/dist/commands/skills.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homePath } from "../adapters/detect.js";
|
|
|
4
4
|
import { detectAgents } from "../adapters/index.js";
|
|
5
5
|
import { readConfig } from "../lib/config.js";
|
|
6
6
|
import { THEMES, style } from "../ui/theme.js";
|
|
7
|
-
import { blank,
|
|
7
|
+
import { blank, line, muted, status, welcome } from "../ui/layout.js";
|
|
8
8
|
/**
|
|
9
9
|
* Where each agent keeps installed skills. Read-only lookups — the CLI lists
|
|
10
10
|
* what is on disk and never infers a skill that is not there.
|
|
@@ -40,7 +40,7 @@ export async function skillsCommand() {
|
|
|
40
40
|
const theme = THEMES[readConfig().theme];
|
|
41
41
|
const agents = await detectAgents();
|
|
42
42
|
blank();
|
|
43
|
-
|
|
43
|
+
welcome(theme, "Lazy Skill");
|
|
44
44
|
blank();
|
|
45
45
|
let total = 0;
|
|
46
46
|
for (const agent of agents) {
|
package/dist/commands/status.js
CHANGED
|
@@ -2,13 +2,13 @@ import { api, ApiError } from "../lib/api.js";
|
|
|
2
2
|
import { readConfig } from "../lib/config.js";
|
|
3
3
|
import { detectAgents } from "../adapters/index.js";
|
|
4
4
|
import { THEMES, style } from "../ui/theme.js";
|
|
5
|
-
import { blank,
|
|
5
|
+
import { blank, line, muted, status, welcome } from "../ui/layout.js";
|
|
6
6
|
export async function statusCommand() {
|
|
7
7
|
const config = readConfig();
|
|
8
8
|
const theme = THEMES[config.theme];
|
|
9
9
|
const agents = await detectAgents();
|
|
10
10
|
blank();
|
|
11
|
-
|
|
11
|
+
welcome(theme, "Lazy Skill");
|
|
12
12
|
blank();
|
|
13
13
|
if (!config.deviceToken) {
|
|
14
14
|
status("computer", "off", "not connected");
|
package/dist/commands/theme.js
CHANGED
|
@@ -1,47 +1,62 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { readConfig, updateConfig } from "../lib/config.js";
|
|
3
3
|
import { THEMES, THEME_IDS, rgb, style } from "../ui/theme.js";
|
|
4
|
+
import { blank, box, muted, ok, welcome } from "../ui/layout.js";
|
|
4
5
|
/**
|
|
5
6
|
* Theme picker. The choice is stored locally and travels with the next
|
|
6
|
-
* pairing so the phone
|
|
7
|
+
* pairing, so the phone ends up on the same colour.
|
|
7
8
|
*/
|
|
8
9
|
export async function themeCommand(requested) {
|
|
9
10
|
const current = readConfig().theme;
|
|
11
|
+
const theme = THEMES[current];
|
|
10
12
|
if (requested) {
|
|
11
13
|
const match = THEME_IDS.find((id) => id === requested);
|
|
12
14
|
if (!match) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
blank();
|
|
16
|
+
muted(`Unknown theme "${requested}".`);
|
|
17
|
+
muted(THEME_IDS.join(", "));
|
|
18
|
+
blank();
|
|
15
19
|
return 1;
|
|
16
20
|
}
|
|
17
21
|
updateConfig({ theme: match });
|
|
18
|
-
|
|
22
|
+
blank();
|
|
23
|
+
ok(`Theme set to ${rgb(THEMES[match].accent, THEMES[match].label)}.`);
|
|
24
|
+
muted("Connect again to carry it to your phone.");
|
|
25
|
+
blank();
|
|
19
26
|
return 0;
|
|
20
27
|
}
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
blank();
|
|
29
|
+
welcome(theme, "Pick a colour");
|
|
30
|
+
blank();
|
|
31
|
+
box(theme, THEME_IDS.map((id, i) => {
|
|
23
32
|
const t = THEMES[id];
|
|
24
33
|
const marker = id === current ? style.green("●") : style.gray("○");
|
|
25
|
-
|
|
26
|
-
});
|
|
27
|
-
|
|
34
|
+
return `${marker} ${style.gray(String(i + 1))} ${rgb(t.accent, "████")} ${t.label}`;
|
|
35
|
+
}));
|
|
36
|
+
blank();
|
|
28
37
|
if (!process.stdin.isTTY) {
|
|
29
|
-
|
|
38
|
+
muted("Not a terminal — pass an id, e.g. lazy-skill theme matrix-green");
|
|
39
|
+
blank();
|
|
30
40
|
return 0;
|
|
31
41
|
}
|
|
32
42
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
33
43
|
try {
|
|
34
|
-
const answer = (await rl.question(
|
|
44
|
+
const answer = (await rl.question(` Number ${style.gray("(enter to keep)")} `)).trim();
|
|
35
45
|
if (!answer)
|
|
36
46
|
return 0;
|
|
37
47
|
const index = Number(answer) - 1;
|
|
38
48
|
if (!Number.isInteger(index) || index < 0 || index >= THEME_IDS.length) {
|
|
39
|
-
|
|
49
|
+
blank();
|
|
50
|
+
muted("Not one of those. Theme unchanged.");
|
|
51
|
+
blank();
|
|
40
52
|
return 1;
|
|
41
53
|
}
|
|
42
54
|
const chosen = THEME_IDS[index];
|
|
43
55
|
updateConfig({ theme: chosen });
|
|
44
|
-
|
|
56
|
+
blank();
|
|
57
|
+
ok(`Theme set to ${rgb(THEMES[chosen].accent, THEMES[chosen].label)}.`);
|
|
58
|
+
muted("Connect again to carry it to your phone.");
|
|
59
|
+
blank();
|
|
45
60
|
return 0;
|
|
46
61
|
}
|
|
47
62
|
finally {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readConfig } from "./lib/config.js";
|
|
2
|
+
import { isFirstRun, readConfig, updateConfig } from "./lib/config.js";
|
|
3
|
+
import { chooseTheme } from "./ui/onboarding.js";
|
|
3
4
|
import { THEMES, rgb, style } from "./ui/theme.js";
|
|
4
5
|
import { connectCommand } from "./commands/connect.js";
|
|
5
6
|
import { disconnectCommand } from "./commands/disconnect.js";
|
|
@@ -9,7 +10,7 @@ import { installCommand } from "./commands/install.js";
|
|
|
9
10
|
import { themeCommand } from "./commands/theme.js";
|
|
10
11
|
import { listenCommand } from "./commands/listen.js";
|
|
11
12
|
import { warnIfOutdated } from "./lib/version-check.js";
|
|
12
|
-
const VERSION = "0.
|
|
13
|
+
const VERSION = "0.4.0";
|
|
13
14
|
function help() {
|
|
14
15
|
const theme = THEMES[readConfig().theme];
|
|
15
16
|
const cmd = (name) => rgb(theme.accent, name.padEnd(26));
|
|
@@ -60,6 +61,13 @@ async function main() {
|
|
|
60
61
|
// Only for the long-running commands: a stale copy matters most when the
|
|
61
62
|
// user is about to sit and wait for something that will never work.
|
|
62
63
|
const longRunning = !command || command === "connect" || command === "listen";
|
|
64
|
+
// Ask for a colour before anything is drawn, so the first thing the user
|
|
65
|
+
// sees is already in their theme — and so the phone has something to adopt
|
|
66
|
+
// when it pairs a moment later. Only on a genuinely first run, and never
|
|
67
|
+
// for `theme`, which asks on its own.
|
|
68
|
+
if (isFirstRun() && command !== "theme") {
|
|
69
|
+
updateConfig({ theme: await chooseTheme() });
|
|
70
|
+
}
|
|
63
71
|
if (longRunning)
|
|
64
72
|
await warnIfOutdated(VERSION, THEMES[readConfig().theme]);
|
|
65
73
|
if (!command) {
|
package/dist/lib/config.js
CHANGED
|
@@ -8,6 +8,10 @@ const DEFAULTS = { theme: DEFAULT_THEME };
|
|
|
8
8
|
export function configPath() {
|
|
9
9
|
return FILE;
|
|
10
10
|
}
|
|
11
|
+
/** True before the user has ever been asked anything. */
|
|
12
|
+
export function isFirstRun() {
|
|
13
|
+
return !existsSync(FILE);
|
|
14
|
+
}
|
|
11
15
|
export function readConfig() {
|
|
12
16
|
try {
|
|
13
17
|
if (!existsSync(FILE))
|
package/dist/ui/layout.js
CHANGED
|
@@ -2,30 +2,33 @@ import { rgb, style, visibleWidth } from "./theme.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* The CLI's visual language.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* One box, around the thing the user is meant to act on — the QR, a prompt, a
|
|
6
|
+
* result. Everything else is indented text. Framing every line was the
|
|
7
|
+
* problem before: two border glyphs per row, a fixed width, and content
|
|
8
|
+
* squeezed between them.
|
|
9
9
|
*/
|
|
10
|
-
const PAD = " ";
|
|
10
|
+
export const PAD = " ";
|
|
11
|
+
/** Box width, clamped so it stays readable in a narrow terminal. */
|
|
12
|
+
export function boxWidth() {
|
|
13
|
+
const columns = process.stdout.columns ?? 80;
|
|
14
|
+
return Math.max(44, Math.min(72, columns - 4));
|
|
15
|
+
}
|
|
11
16
|
export function blank() {
|
|
12
17
|
console.log();
|
|
13
18
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
console.log(`${PAD}${style.bold("LAZY")} ${rgb(theme.accent, style.bold("SKILL"))}${rgb(theme.support, " Zz")}`);
|
|
19
|
+
export function line(text = "") {
|
|
20
|
+
console.log(text ? `${PAD}${text}` : "");
|
|
17
21
|
}
|
|
18
|
-
/** A quiet line of supporting text. */
|
|
19
22
|
export function muted(text) {
|
|
20
23
|
console.log(`${PAD}${style.gray(text)}`);
|
|
21
24
|
}
|
|
22
|
-
/**
|
|
23
|
-
export function
|
|
24
|
-
console.log(
|
|
25
|
+
/** The welcome line. The mark, then the product, then a greeting. */
|
|
26
|
+
export function welcome(theme, text) {
|
|
27
|
+
console.log(`${PAD}${rgb(theme.accent, "✻")} ${style.bold(text)}`);
|
|
25
28
|
}
|
|
26
|
-
/**
|
|
27
|
-
export function
|
|
28
|
-
console.log(`${PAD}${
|
|
29
|
+
/** An indented hint under the welcome. */
|
|
30
|
+
export function hint(text) {
|
|
31
|
+
console.log(`${PAD} ${style.gray(text)}`);
|
|
29
32
|
}
|
|
30
33
|
const DOT = {
|
|
31
34
|
ok: style.green("●"),
|
|
@@ -33,7 +36,6 @@ const DOT = {
|
|
|
33
36
|
off: style.gray("○"),
|
|
34
37
|
fail: style.red("●"),
|
|
35
38
|
};
|
|
36
|
-
/** `● Claude ready` — the dot carries the state, the word confirms it. */
|
|
37
39
|
export function status(label, tone, detail = "") {
|
|
38
40
|
const width = 10;
|
|
39
41
|
const name = label.length >= width ? label : label + " ".repeat(width - label.length);
|
|
@@ -44,18 +46,6 @@ export function status(label, tone, detail = "") {
|
|
|
44
46
|
: style.gray(detail || "not found");
|
|
45
47
|
console.log(`${PAD}${DOT[tone]} ${name} ${text}`);
|
|
46
48
|
}
|
|
47
|
-
/** A step in a sequence, numbered so the order is obvious. */
|
|
48
|
-
export function step(theme, n, text) {
|
|
49
|
-
console.log(`${PAD}${rgb(theme.accent, String(n))} ${text}`);
|
|
50
|
-
}
|
|
51
|
-
/** Something the user is meant to type or scan. */
|
|
52
|
-
export function emphasis(theme, text) {
|
|
53
|
-
console.log(`${PAD}${rgb(theme.accent, text)}`);
|
|
54
|
-
}
|
|
55
|
-
/** A short rule, only where a real break is needed. */
|
|
56
|
-
export function divider(width = 34) {
|
|
57
|
-
console.log(`${PAD}${style.gray("─".repeat(width))}`);
|
|
58
|
-
}
|
|
59
49
|
export function ok(text) {
|
|
60
50
|
console.log(`${PAD}${style.green("✓")} ${text}`);
|
|
61
51
|
}
|
|
@@ -65,4 +55,26 @@ export function warn(text) {
|
|
|
65
55
|
export function fail(text) {
|
|
66
56
|
console.log(`${PAD}${style.red("✗")} ${text}`);
|
|
67
57
|
}
|
|
68
|
-
|
|
58
|
+
/**
|
|
59
|
+
* The one box.
|
|
60
|
+
*
|
|
61
|
+
* Rows are padded to a common width using their *visible* length, so colour
|
|
62
|
+
* codes inside the content cannot push the right border out of alignment.
|
|
63
|
+
*/
|
|
64
|
+
export function box(theme, rows, width = boxWidth()) {
|
|
65
|
+
const edge = (left, right) => console.log(`${PAD}${rgb(theme.accent, left + "─".repeat(width - 2) + right)}`);
|
|
66
|
+
const bar = rgb(theme.accent, "│");
|
|
67
|
+
edge("╭", "╮");
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const pad = Math.max(0, width - 4 - visibleWidth(row));
|
|
70
|
+
console.log(`${PAD}${bar} ${row}${" ".repeat(pad)} ${bar}`);
|
|
71
|
+
}
|
|
72
|
+
edge("╰", "╯");
|
|
73
|
+
}
|
|
74
|
+
/** Centres a row inside the box. */
|
|
75
|
+
export function centre(text, width = boxWidth()) {
|
|
76
|
+
const inner = width - 4;
|
|
77
|
+
const left = Math.max(0, Math.floor((inner - visibleWidth(text)) / 2));
|
|
78
|
+
return " ".repeat(left) + text;
|
|
79
|
+
}
|
|
80
|
+
export { visibleWidth };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { THEMES, THEME_IDS, rgb, style } from "./theme.js";
|
|
3
|
+
import { blank, box, hint, line, muted, welcome } from "./layout.js";
|
|
4
|
+
/**
|
|
5
|
+
* First-run theme choice.
|
|
6
|
+
*
|
|
7
|
+
* Asked once, before anything else, because the answer applies to both halves
|
|
8
|
+
* of the product: the terminal colours itself, and the phone adopts the same
|
|
9
|
+
* theme the moment it pairs. Choosing later in two places would be two
|
|
10
|
+
* chances to end up mismatched.
|
|
11
|
+
*/
|
|
12
|
+
export async function chooseTheme() {
|
|
13
|
+
const fallback = "cyber-purple";
|
|
14
|
+
const theme = THEMES[fallback];
|
|
15
|
+
blank();
|
|
16
|
+
welcome(theme, "Welcome to Lazy Skill!");
|
|
17
|
+
blank();
|
|
18
|
+
hint("Pick a colour. Your phone will match it when you connect.");
|
|
19
|
+
blank();
|
|
20
|
+
box(theme, THEME_IDS.map((id, i) => {
|
|
21
|
+
const t = THEMES[id];
|
|
22
|
+
return `${style.gray(String(i + 1))} ${rgb(t.accent, "████")} ${t.label}`;
|
|
23
|
+
}));
|
|
24
|
+
blank();
|
|
25
|
+
// A pipe or a CI job has nobody to ask.
|
|
26
|
+
if (!process.stdin.isTTY) {
|
|
27
|
+
muted("Not a terminal — using Purple Night. Change it with: lazy-skill theme");
|
|
28
|
+
blank();
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
+
try {
|
|
33
|
+
const answer = (await rl.question(` Number ${style.gray("(enter for 1)")} `)).trim();
|
|
34
|
+
if (!answer)
|
|
35
|
+
return fallback;
|
|
36
|
+
const index = Number(answer) - 1;
|
|
37
|
+
if (!Number.isInteger(index) || index < 0 || index >= THEME_IDS.length) {
|
|
38
|
+
// Not worth a re-prompt: the choice is cosmetic and changeable.
|
|
39
|
+
line(style.gray(" Not one of those — using Purple Night."));
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
return THEME_IDS[index];
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|