lazy-skill 0.2.1 → 0.3.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 +47 -78
- package/dist/commands/install.js +23 -34
- package/dist/commands/skills.js +18 -24
- package/dist/commands/status.js +21 -29
- package/dist/index.js +1 -1
- package/dist/ui/layout.js +68 -0
- package/dist/ui/qr.js +31 -19
- package/package.json +1 -1
- package/dist/ui/box.js +0 -56
package/dist/commands/connect.js
CHANGED
|
@@ -2,9 +2,9 @@ import { hostname, platform, release } from "node:os";
|
|
|
2
2
|
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
|
-
import { THEMES,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { THEMES, style } from "../ui/theme.js";
|
|
6
|
+
import { renderFramedQr } from "../ui/qr.js";
|
|
7
|
+
import { blank, brand, fail, heading, line, muted, ok, status } from "../ui/layout.js";
|
|
8
8
|
import { messages } from "../ui/messages.js";
|
|
9
9
|
import { Spinner } from "../ui/spinner.js";
|
|
10
10
|
function friendlyPlatform() {
|
|
@@ -14,43 +14,33 @@ function friendlyPlatform() {
|
|
|
14
14
|
function deviceName() {
|
|
15
15
|
return hostname().replace(/\.local$/, "").slice(0, 80) || "This computer";
|
|
16
16
|
}
|
|
17
|
-
function
|
|
18
|
-
console.log();
|
|
19
|
-
console.log(top(theme));
|
|
20
|
-
console.log(row(theme));
|
|
21
|
-
for (const line of wordmark(theme))
|
|
22
|
-
console.log(centered(theme, line));
|
|
23
|
-
console.log(centered(theme, style.dim("Too lazy to install manually? Same.")));
|
|
24
|
-
console.log(row(theme));
|
|
25
|
-
}
|
|
26
|
-
function printAgents(theme, agents) {
|
|
27
|
-
console.log(row(theme, style.bold("Detected AI tools")));
|
|
28
|
-
console.log(row(theme));
|
|
17
|
+
function printAgents(agents) {
|
|
29
18
|
for (const agent of agents) {
|
|
30
|
-
|
|
19
|
+
status(agent.label, agent.detected ? "ok" : "off");
|
|
31
20
|
}
|
|
32
21
|
}
|
|
33
22
|
export async function connectCommand(options) {
|
|
34
23
|
const config = readConfig();
|
|
35
24
|
const theme = THEMES[config.theme];
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
blank();
|
|
26
|
+
brand(theme);
|
|
27
|
+
muted("See it. Search it. Install it.");
|
|
28
|
+
blank();
|
|
29
|
+
// Detection runs before anything is drawn: a spinner writes to the current
|
|
30
|
+
// line, and anything transient printed mid-layout tears it.
|
|
31
|
+
const spinner = new Spinner(theme, "looking for your AI tools").start();
|
|
39
32
|
const agents = await detectAgents();
|
|
40
33
|
const detectedIds = agents.filter((a) => a.detected).map((a) => a.id);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.log(row(theme, `${style.gray(">")} ${messages.humanFound()} ${style.green("✓")}`));
|
|
44
|
-
console.log(row(theme, `${style.gray(">")} ${messages.generating()}`));
|
|
45
|
-
console.log(row(theme));
|
|
34
|
+
spinner.stop();
|
|
35
|
+
printAgents(agents);
|
|
46
36
|
if (options.verbose) {
|
|
37
|
+
blank();
|
|
47
38
|
for (const agent of agents) {
|
|
48
39
|
if (agent.evidence)
|
|
49
|
-
|
|
40
|
+
muted(` ${agent.label}: ${agent.evidence}`);
|
|
50
41
|
}
|
|
51
|
-
console.log(row(theme));
|
|
52
42
|
}
|
|
53
|
-
|
|
43
|
+
blank();
|
|
54
44
|
let session;
|
|
55
45
|
try {
|
|
56
46
|
session = await api("/api/pairing/start", {
|
|
@@ -64,26 +54,22 @@ export async function connectCommand(options) {
|
|
|
64
54
|
});
|
|
65
55
|
}
|
|
66
56
|
catch (err) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
console.error(` ${style.red("✗")} ${message}`);
|
|
71
|
-
console.error(` ${style.gray("Set LAZY_SKILL_API_URL if you are pointing at a local server.")}`);
|
|
72
|
-
console.log();
|
|
57
|
+
fail(err instanceof ApiError ? err.message : messages.failed());
|
|
58
|
+
muted("Set LAZY_SKILL_API_URL to point at a different server.");
|
|
59
|
+
blank();
|
|
73
60
|
return 1;
|
|
74
61
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for (const
|
|
79
|
-
console.log(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// --- wait for the phone ---------------------------------------------
|
|
62
|
+
heading(theme, "Scan to connect");
|
|
63
|
+
muted("Open Lazy Skill on your phone and point the camera here.");
|
|
64
|
+
blank();
|
|
65
|
+
for (const row of await renderFramedQr(session.pairUrl))
|
|
66
|
+
console.log(row);
|
|
67
|
+
blank();
|
|
68
|
+
muted(session.pairUrl);
|
|
69
|
+
blank();
|
|
84
70
|
const deadline = Date.now() + session.expiresInMs;
|
|
85
|
-
const
|
|
86
|
-
let
|
|
71
|
+
const waiting = new Spinner(theme, "waiting for scan").start();
|
|
72
|
+
let nudgedAt = Date.now();
|
|
87
73
|
let result = { status: "waiting" };
|
|
88
74
|
while (Date.now() < deadline) {
|
|
89
75
|
await sleep(1500);
|
|
@@ -97,13 +83,12 @@ export async function connectCommand(options) {
|
|
|
97
83
|
}
|
|
98
84
|
if (result.status !== "waiting")
|
|
99
85
|
break;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
lastNudge = Date.now();
|
|
86
|
+
if (Date.now() - nudgedAt > 15_000) {
|
|
87
|
+
waiting.update(messages.waiting());
|
|
88
|
+
nudgedAt = Date.now();
|
|
104
89
|
}
|
|
105
90
|
}
|
|
106
|
-
|
|
91
|
+
waiting.stop();
|
|
107
92
|
if (result.status === "paired" && result.deviceToken) {
|
|
108
93
|
updateConfig({
|
|
109
94
|
deviceToken: result.deviceToken,
|
|
@@ -111,41 +96,25 @@ export async function connectCommand(options) {
|
|
|
111
96
|
deviceName: result.device?.name ?? deviceName(),
|
|
112
97
|
connectedAt: new Date().toISOString(),
|
|
113
98
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
console.log(top(theme));
|
|
118
|
-
console.log(row(theme));
|
|
119
|
-
for (const line of mascot(theme, true))
|
|
120
|
-
console.log(centered(theme, line));
|
|
121
|
-
console.log(row(theme));
|
|
122
|
-
console.log(centered(theme, rgb(theme.accent, style.bold(messages.connected()))));
|
|
123
|
-
console.log(row(theme));
|
|
124
|
-
printAgents(theme, agents);
|
|
125
|
-
console.log(row(theme));
|
|
126
|
-
console.log(centered(theme, style.dim(messages.signOff())));
|
|
127
|
-
console.log(row(theme));
|
|
128
|
-
console.log(bottom(theme));
|
|
129
|
-
console.log();
|
|
130
|
-
console.log(` ${style.gray("Credential stored in ~/.lazyskill/config.json (0600).")}`);
|
|
131
|
-
// Keep listening by default. Someone who has just paired a computer and
|
|
132
|
-
// reached for their phone wants an install to land, not a second command
|
|
133
|
-
// to remember — a queued job with nothing polling for it just looks broken.
|
|
99
|
+
ok(style.bold("Connected"));
|
|
100
|
+
line(style.gray(result.device?.name ?? deviceName()));
|
|
101
|
+
blank();
|
|
134
102
|
if (options.listen === false) {
|
|
135
|
-
|
|
136
|
-
|
|
103
|
+
muted("Run lazy-skill when you want to install from your phone.");
|
|
104
|
+
blank();
|
|
137
105
|
return 0;
|
|
138
106
|
}
|
|
139
|
-
console.log();
|
|
140
107
|
const { listenCommand } = await import("./listen.js");
|
|
141
108
|
return listenCommand();
|
|
142
109
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
110
|
+
if (result.status === "consumed") {
|
|
111
|
+
fail("That code was already used.");
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
fail(messages.expired());
|
|
115
|
+
}
|
|
116
|
+
muted("Run lazy-skill for a fresh one.");
|
|
117
|
+
blank();
|
|
149
118
|
return 1;
|
|
150
119
|
}
|
|
151
120
|
function sleep(ms) {
|
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 {
|
|
5
|
+
import { blank, brand, fail, line, muted, ok, status } from "../ui/layout.js";
|
|
6
6
|
import { messages } from "../ui/messages.js";
|
|
7
7
|
const STAGE_LABEL = {
|
|
8
8
|
starting: "Preparing",
|
|
@@ -86,18 +86,15 @@ export async function installCommand(ref, options = { yes: false }) {
|
|
|
86
86
|
return 1;
|
|
87
87
|
}
|
|
88
88
|
// Show exactly what will run, before it runs.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log(row(theme));
|
|
99
|
-
console.log(bottom(theme));
|
|
100
|
-
console.log();
|
|
89
|
+
blank();
|
|
90
|
+
brand(theme);
|
|
91
|
+
blank();
|
|
92
|
+
line(`${style.bold(ref)}`);
|
|
93
|
+
muted(`for ${targets.map((t) => t.label).join(", ")} · ${options.project ? "this project" : "global"}`);
|
|
94
|
+
blank();
|
|
95
|
+
muted("This will run:");
|
|
96
|
+
line(rgb(theme.accent, describePlan(plan)));
|
|
97
|
+
blank();
|
|
101
98
|
if (!options.yes && !(await confirm("Continue?"))) {
|
|
102
99
|
console.log(` ${style.gray("Cancelled. Nothing was installed.")}\n`);
|
|
103
100
|
return 1;
|
|
@@ -111,7 +108,7 @@ export async function installCommand(ref, options = { yes: false }) {
|
|
|
111
108
|
if (progress.stage === "done" || progress.stage === "failed")
|
|
112
109
|
return;
|
|
113
110
|
const label = STAGE_LABEL[progress.stage] ?? progress.stage;
|
|
114
|
-
|
|
111
|
+
line(`${style.gray(label.padEnd(12))} ${progressBar(theme, progress.stage)}`);
|
|
115
112
|
};
|
|
116
113
|
// One adapter at a time, so a failure names the agent it belongs to.
|
|
117
114
|
const failures = [];
|
|
@@ -124,28 +121,20 @@ export async function installCommand(ref, options = { yes: false }) {
|
|
|
124
121
|
}
|
|
125
122
|
console.log();
|
|
126
123
|
if (failures.length > 0) {
|
|
127
|
-
|
|
124
|
+
fail(messages.failed());
|
|
128
125
|
for (const failure of failures)
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
muted(failure);
|
|
127
|
+
blank();
|
|
131
128
|
return 1;
|
|
132
129
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
console.log(row(theme, `${target.label.padEnd(12)} ${style.green("✓")}`));
|
|
143
|
-
}
|
|
144
|
-
console.log(row(theme));
|
|
145
|
-
console.log(row(theme, `${style.gray("You contributed")} ${style.bold("0% effort")}`));
|
|
146
|
-
console.log(row(theme, `${style.gray("Lazy Skill")} ${rgb(theme.accent, "100% effort")}`));
|
|
147
|
-
console.log(row(theme));
|
|
148
|
-
console.log(bottom(theme));
|
|
149
|
-
console.log();
|
|
130
|
+
blank();
|
|
131
|
+
ok(style.bold("Installed"));
|
|
132
|
+
line(style.gray(ref));
|
|
133
|
+
blank();
|
|
134
|
+
for (const target of targets)
|
|
135
|
+
status(target.label, "ok", "installed");
|
|
136
|
+
blank();
|
|
137
|
+
muted("You contributed 0% effort. Lazy Skill did the rest.");
|
|
138
|
+
blank();
|
|
150
139
|
return 0;
|
|
151
140
|
}
|
package/dist/commands/skills.js
CHANGED
|
@@ -4,10 +4,10 @@ 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 {
|
|
7
|
+
import { blank, brand, line, muted, status } from "../ui/layout.js";
|
|
8
8
|
/**
|
|
9
|
-
* Where each agent keeps installed skills.
|
|
10
|
-
*
|
|
9
|
+
* Where each agent keeps installed skills. Read-only lookups — the CLI lists
|
|
10
|
+
* what is on disk and never infers a skill that is not there.
|
|
11
11
|
*/
|
|
12
12
|
const SKILL_DIRS = {
|
|
13
13
|
claude: [homePath(".claude", "skills")],
|
|
@@ -39,36 +39,30 @@ function listSkillDirs(paths) {
|
|
|
39
39
|
export async function skillsCommand() {
|
|
40
40
|
const theme = THEMES[readConfig().theme];
|
|
41
41
|
const agents = await detectAgents();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
for (const line of wordmark(theme))
|
|
46
|
-
console.log(centered(theme, line));
|
|
47
|
-
console.log(row(theme));
|
|
48
|
-
console.log(row(theme, style.bold("Installed skills on this computer")));
|
|
49
|
-
console.log(row(theme));
|
|
42
|
+
blank();
|
|
43
|
+
brand(theme);
|
|
44
|
+
blank();
|
|
50
45
|
let total = 0;
|
|
51
46
|
for (const agent of agents) {
|
|
52
47
|
if (!agent.detected) {
|
|
53
|
-
|
|
48
|
+
status(agent.label, "off");
|
|
54
49
|
continue;
|
|
55
50
|
}
|
|
56
51
|
const skills = listSkillDirs(SKILL_DIRS[agent.id] ?? []);
|
|
57
52
|
total += skills.length;
|
|
58
|
-
|
|
59
|
-
for (const skill of skills.slice(0,
|
|
60
|
-
|
|
53
|
+
status(agent.label, "ok", skills.length ? `${skills.length} installed` : "none installed");
|
|
54
|
+
for (const skill of skills.slice(0, 12)) {
|
|
55
|
+
line(style.gray(` ${skill}`));
|
|
61
56
|
}
|
|
62
|
-
if (skills.length >
|
|
63
|
-
|
|
57
|
+
if (skills.length > 12) {
|
|
58
|
+
line(style.gray(` and ${skills.length - 12} more`));
|
|
64
59
|
}
|
|
65
60
|
}
|
|
66
|
-
|
|
67
|
-
if (total === 0)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
console.log();
|
|
61
|
+
blank();
|
|
62
|
+
if (total === 0)
|
|
63
|
+
muted("Nothing installed yet. You haven't been lazy enough.");
|
|
64
|
+
else
|
|
65
|
+
muted(`${total} skill${total === 1 ? "" : "s"} across your tools.`);
|
|
66
|
+
blank();
|
|
73
67
|
return 0;
|
|
74
68
|
}
|
package/dist/commands/status.js
CHANGED
|
@@ -2,54 +2,46 @@ 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 {
|
|
5
|
+
import { blank, brand, line, muted, status } 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
for (const line of wordmark(theme))
|
|
14
|
-
console.log(centered(theme, line));
|
|
15
|
-
console.log(row(theme));
|
|
10
|
+
blank();
|
|
11
|
+
brand(theme);
|
|
12
|
+
blank();
|
|
16
13
|
if (!config.deviceToken) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
console.log(bottom(theme));
|
|
22
|
-
console.log();
|
|
14
|
+
status("computer", "off", "not connected");
|
|
15
|
+
blank();
|
|
16
|
+
muted("Run lazy-skill to pair.");
|
|
17
|
+
blank();
|
|
23
18
|
return 1;
|
|
24
19
|
}
|
|
25
|
-
// Detection is local and always shown. The
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
let
|
|
20
|
+
// Detection is local and always shown. The round trip only confirms the
|
|
21
|
+
// credential still works, so a network failure downgrades the report rather
|
|
22
|
+
// than hiding what we already know.
|
|
23
|
+
let reachable = false;
|
|
29
24
|
let remoteError = null;
|
|
30
25
|
try {
|
|
31
|
-
|
|
26
|
+
await api("/api/cli/status", {
|
|
32
27
|
authed: true,
|
|
33
28
|
body: { detectedAgents: agents.filter((a) => a.detected).map((a) => a.id) },
|
|
34
29
|
});
|
|
30
|
+
reachable = true;
|
|
35
31
|
}
|
|
36
32
|
catch (err) {
|
|
37
33
|
remoteError = err instanceof ApiError ? err.message : "Could not reach the server.";
|
|
38
34
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: `${style.yellow("●")} Credential stored, server unreachable`));
|
|
43
|
-
console.log(row(theme));
|
|
35
|
+
line(style.bold(config.deviceName ?? "This computer"));
|
|
36
|
+
status("connection", reachable ? "ok" : "pending", reachable ? "connected" : "stored, server unreachable");
|
|
37
|
+
blank();
|
|
44
38
|
for (const agent of agents) {
|
|
45
|
-
|
|
39
|
+
status(agent.label, agent.detected ? "ok" : "off");
|
|
46
40
|
}
|
|
47
|
-
|
|
48
|
-
console.log(bottom(theme));
|
|
41
|
+
blank();
|
|
49
42
|
if (remoteError) {
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
muted(remoteError);
|
|
44
|
+
blank();
|
|
52
45
|
}
|
|
53
|
-
console.log();
|
|
54
46
|
return 0;
|
|
55
47
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { installCommand } from "./commands/install.js";
|
|
|
9
9
|
import { themeCommand } from "./commands/theme.js";
|
|
10
10
|
import { listenCommand } from "./commands/listen.js";
|
|
11
11
|
import { warnIfOutdated } from "./lib/version-check.js";
|
|
12
|
-
const VERSION = "0.
|
|
12
|
+
const VERSION = "0.3.0";
|
|
13
13
|
function help() {
|
|
14
14
|
const theme = THEMES[readConfig().theme];
|
|
15
15
|
const cmd = (name) => rgb(theme.accent, name.padEnd(26));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { rgb, style, visibleWidth } from "./theme.js";
|
|
2
|
+
/**
|
|
3
|
+
* The CLI's visual language.
|
|
4
|
+
*
|
|
5
|
+
* No frames. The old design wrapped everything in a box, which meant every
|
|
6
|
+
* line carried two border glyphs and a fixed width, and the content had to
|
|
7
|
+
* fight for the space between them. Indentation and blank lines do the same
|
|
8
|
+
* grouping work without the noise, and they reflow on any terminal size.
|
|
9
|
+
*/
|
|
10
|
+
const PAD = " ";
|
|
11
|
+
export function blank() {
|
|
12
|
+
console.log();
|
|
13
|
+
}
|
|
14
|
+
/** The wordmark. Two-tone, matching the web app. */
|
|
15
|
+
export function brand(theme) {
|
|
16
|
+
console.log(`${PAD}${style.bold("LAZY")} ${rgb(theme.accent, style.bold("SKILL"))}${rgb(theme.support, " Zz")}`);
|
|
17
|
+
}
|
|
18
|
+
/** A quiet line of supporting text. */
|
|
19
|
+
export function muted(text) {
|
|
20
|
+
console.log(`${PAD}${style.gray(text)}`);
|
|
21
|
+
}
|
|
22
|
+
/** A normal line. */
|
|
23
|
+
export function line(text = "") {
|
|
24
|
+
console.log(text ? `${PAD}${text}` : "");
|
|
25
|
+
}
|
|
26
|
+
/** A heading, used sparingly — one per screen at most. */
|
|
27
|
+
export function heading(theme, text) {
|
|
28
|
+
console.log(`${PAD}${rgb(theme.accent, style.bold(text))}`);
|
|
29
|
+
}
|
|
30
|
+
const DOT = {
|
|
31
|
+
ok: style.green("●"),
|
|
32
|
+
pending: style.yellow("●"),
|
|
33
|
+
off: style.gray("○"),
|
|
34
|
+
fail: style.red("●"),
|
|
35
|
+
};
|
|
36
|
+
/** `● Claude ready` — the dot carries the state, the word confirms it. */
|
|
37
|
+
export function status(label, tone, detail = "") {
|
|
38
|
+
const width = 10;
|
|
39
|
+
const name = label.length >= width ? label : label + " ".repeat(width - label.length);
|
|
40
|
+
const text = tone === "ok"
|
|
41
|
+
? style.green(detail || "ready")
|
|
42
|
+
: tone === "fail"
|
|
43
|
+
? style.red(detail || "failed")
|
|
44
|
+
: style.gray(detail || "not found");
|
|
45
|
+
console.log(`${PAD}${DOT[tone]} ${name} ${text}`);
|
|
46
|
+
}
|
|
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
|
+
export function ok(text) {
|
|
60
|
+
console.log(`${PAD}${style.green("✓")} ${text}`);
|
|
61
|
+
}
|
|
62
|
+
export function warn(text) {
|
|
63
|
+
console.log(`${PAD}${style.yellow("!")} ${text}`);
|
|
64
|
+
}
|
|
65
|
+
export function fail(text) {
|
|
66
|
+
console.log(`${PAD}${style.red("✗")} ${text}`);
|
|
67
|
+
}
|
|
68
|
+
export { PAD, visibleWidth };
|
package/dist/ui/qr.js
CHANGED
|
@@ -2,6 +2,18 @@ import QRCode from "qrcode";
|
|
|
2
2
|
/** The terminal renderer paints black modules on a white background. */
|
|
3
3
|
const QR_OPEN = "\x1b[47m\x1b[30m";
|
|
4
4
|
const QR_CLOSE = "\x1b[0m";
|
|
5
|
+
// eslint-disable-next-line no-control-regex
|
|
6
|
+
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
7
|
+
function stripAnsi(text) {
|
|
8
|
+
return text.replace(ANSI, "");
|
|
9
|
+
}
|
|
10
|
+
/** Visible cell count, ignoring escape sequences. */
|
|
11
|
+
export function visibleLength(text) {
|
|
12
|
+
return stripAnsi(text).length;
|
|
13
|
+
}
|
|
14
|
+
function escapeRe(text) {
|
|
15
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16
|
+
}
|
|
5
17
|
/**
|
|
6
18
|
* Renders the pairing QR.
|
|
7
19
|
*
|
|
@@ -10,12 +22,12 @@ const QR_CLOSE = "\x1b[0m";
|
|
|
10
22
|
* reliability is not negotiable for decoration (§10).
|
|
11
23
|
*
|
|
12
24
|
* The quiet zone is added here rather than passed as an option: node-qrcode's
|
|
13
|
-
* terminal renderer ignores `margin` when `small` is set
|
|
14
|
-
* single module of padding
|
|
15
|
-
* patterns sit flush against surrounding
|
|
16
|
-
* scanning. Each rendered row is two modules tall
|
|
17
|
-
*
|
|
18
|
-
*
|
|
25
|
+
* terminal renderer ignores `margin` when `small` is set and ships only a
|
|
26
|
+
* single module of padding, where the standard wants four. Without it the
|
|
27
|
+
* finder patterns sit flush against surrounding text, which measurably hurts
|
|
28
|
+
* scanning. Each rendered row is two modules tall, so two blank rows above and
|
|
29
|
+
* below give four modules vertically, and three extra columns per side top up
|
|
30
|
+
* the one already present.
|
|
19
31
|
*/
|
|
20
32
|
export async function renderQr(url) {
|
|
21
33
|
const art = await QRCode.toString(url, {
|
|
@@ -25,11 +37,11 @@ export async function renderQr(url) {
|
|
|
25
37
|
});
|
|
26
38
|
const rows = art
|
|
27
39
|
.split("\n")
|
|
28
|
-
.filter((
|
|
40
|
+
.filter((row) => stripAnsi(row).length > 0)
|
|
29
41
|
// Drop only the wrapper this renderer adds. Interior codes are left alone:
|
|
30
42
|
// the final half-row recolours per cell, and rewriting that would corrupt
|
|
31
43
|
// the modules themselves.
|
|
32
|
-
.map((
|
|
44
|
+
.map((row) => row.replace(new RegExp(`^${escapeRe(QR_OPEN)}`), "").replace(/\x1b\[0m$/, ""));
|
|
33
45
|
if (rows.length === 0)
|
|
34
46
|
return [];
|
|
35
47
|
const SIDE = 3;
|
|
@@ -45,15 +57,15 @@ export async function renderQr(url) {
|
|
|
45
57
|
});
|
|
46
58
|
return [blank, blank, ...padded, blank, blank];
|
|
47
59
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return
|
|
60
|
+
/**
|
|
61
|
+
* The QR, indented to sit with the rest of the output.
|
|
62
|
+
*
|
|
63
|
+
* No frame. The white plate is already a clean, high-contrast rectangle with
|
|
64
|
+
* its own quiet zone — drawing rails around it added a border glyph to every
|
|
65
|
+
* single row and made the densest thing on screen denser still. Indentation
|
|
66
|
+
* places it; nothing else is needed.
|
|
67
|
+
*/
|
|
68
|
+
export async function renderFramedQr(url) {
|
|
69
|
+
const plate = await renderQr(url);
|
|
70
|
+
return plate.map((row) => ` ${row}`);
|
|
59
71
|
}
|
package/package.json
CHANGED
package/dist/ui/box.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { rgb, style, visibleWidth } from "./theme.js";
|
|
2
|
-
/**
|
|
3
|
-
* The framed card the CLI draws. Rounded corners and a themed border, to match
|
|
4
|
-
* the cards on the brand board.
|
|
5
|
-
*/
|
|
6
|
-
const B = {
|
|
7
|
-
tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│",
|
|
8
|
-
};
|
|
9
|
-
export const CARD_WIDTH = 56;
|
|
10
|
-
export function rule(theme, width = CARD_WIDTH) {
|
|
11
|
-
return rgb(theme.accent, B.h.repeat(width - 2));
|
|
12
|
-
}
|
|
13
|
-
export function top(theme, width = CARD_WIDTH) {
|
|
14
|
-
return rgb(theme.accent, B.tl + B.h.repeat(width - 2) + B.tr);
|
|
15
|
-
}
|
|
16
|
-
export function bottom(theme, width = CARD_WIDTH) {
|
|
17
|
-
return rgb(theme.accent, B.bl + B.h.repeat(width - 2) + B.br);
|
|
18
|
-
}
|
|
19
|
-
/** A content row, padded to the card width using visible (not raw) length. */
|
|
20
|
-
export function row(theme, content = "", width = CARD_WIDTH) {
|
|
21
|
-
const inner = width - 4;
|
|
22
|
-
const pad = Math.max(0, inner - visibleWidth(content));
|
|
23
|
-
const edge = rgb(theme.accent, B.v);
|
|
24
|
-
return `${edge} ${content}${" ".repeat(pad)} ${edge}`;
|
|
25
|
-
}
|
|
26
|
-
export function centered(theme, content, width = CARD_WIDTH) {
|
|
27
|
-
const inner = width - 4;
|
|
28
|
-
const len = visibleWidth(content);
|
|
29
|
-
const left = Math.max(0, Math.floor((inner - len) / 2));
|
|
30
|
-
return row(theme, " ".repeat(left) + content, width);
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* The wordmark, in the same two-tone split the web app uses: LAZY in the
|
|
34
|
-
* terminal's default ink, SKILL in the theme accent.
|
|
35
|
-
*/
|
|
36
|
-
export function wordmark(theme) {
|
|
37
|
-
return [
|
|
38
|
-
`${style.bold("LAZY")} ${rgb(theme.accent, style.bold("SKILL"))}${rgb(theme.support, " Zz")}`,
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
/** Bit, small enough to sit inside a card. */
|
|
42
|
-
export function mascot(theme, awake = false) {
|
|
43
|
-
const hood = (t) => rgb(theme.accent, t);
|
|
44
|
-
const eyes = awake ? "o o" : "- -";
|
|
45
|
-
return [
|
|
46
|
-
hood(" ▄███▄ "),
|
|
47
|
-
hood(" █") + `\x1b[0m${eyes}` + hood("█ "),
|
|
48
|
-
hood(" █ ") + "‿" + hood(" █ "),
|
|
49
|
-
hood(" ▀███▀ "),
|
|
50
|
-
];
|
|
51
|
-
}
|
|
52
|
-
export function statusLine(label, ok) {
|
|
53
|
-
const mark = ok === null ? style.gray("○") : ok ? style.green("✓") : style.gray("○");
|
|
54
|
-
const text = ok === null ? style.gray("Not detected") : ok ? style.green("Ready") : style.gray("Not detected");
|
|
55
|
-
return `${label.padEnd(12)} ${mark} ${text}`;
|
|
56
|
-
}
|