coherence-cli 0.8.3 → 0.9.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/bin/cc.mjs +27 -0
- package/lib/commands/assets.mjs +18 -5
- package/lib/commands/contributors.mjs +19 -5
- package/lib/commands/deploy.mjs +163 -0
- package/lib/commands/friction.mjs +45 -11
- package/lib/commands/governance.mjs +25 -5
- package/lib/commands/ideas.mjs +24 -8
- package/lib/commands/lineage.mjs +12 -6
- package/lib/commands/listen.mjs +106 -0
- package/lib/commands/news.mjs +26 -9
- package/lib/commands/nodes.mjs +28 -130
- package/lib/commands/providers.mjs +55 -10
- package/lib/commands/services.mjs +14 -6
- package/lib/commands/specs.mjs +13 -5
- package/lib/commands/status.mjs +60 -1
- package/lib/identity.mjs +100 -19
- package/package.json +1 -1
package/lib/commands/news.mjs
CHANGED
|
@@ -4,30 +4,47 @@
|
|
|
4
4
|
|
|
5
5
|
import { get, post } from "../api.mjs";
|
|
6
6
|
|
|
7
|
+
/** Truncate at word boundary, append "..." if needed */
|
|
7
8
|
function truncate(str, len) {
|
|
8
9
|
if (!str) return "";
|
|
9
|
-
|
|
10
|
+
if (str.length <= len) return str;
|
|
11
|
+
const trimmed = str.slice(0, len - 3);
|
|
12
|
+
const lastSpace = trimmed.lastIndexOf(" ");
|
|
13
|
+
return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
|
|
10
14
|
}
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
/** Format relative time from ISO string */
|
|
17
|
+
function timeAgo(isoStr) {
|
|
18
|
+
if (!isoStr) return "";
|
|
19
|
+
const min = Math.floor((Date.now() - new Date(isoStr).getTime()) / 60000);
|
|
20
|
+
if (min < 1) return "just now";
|
|
21
|
+
if (min < 60) return `${min}m ago`;
|
|
22
|
+
if (min < 1440) return `${Math.floor(min / 60)}h ago`;
|
|
23
|
+
return `${Math.floor(min / 1440)}d ago`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function showNewsFeed(args) {
|
|
27
|
+
const limit = (args && parseInt(args[0])) || 10;
|
|
13
28
|
const data = await get("/api/news/feed");
|
|
14
|
-
const
|
|
15
|
-
if (!
|
|
29
|
+
const allItems = Array.isArray(data) ? data : data?.items || data?.articles;
|
|
30
|
+
if (!allItems || !Array.isArray(allItems)) {
|
|
16
31
|
console.log("Could not fetch news feed.");
|
|
17
32
|
return;
|
|
18
33
|
}
|
|
34
|
+
const items = allItems.slice(0, limit);
|
|
19
35
|
if (items.length === 0) {
|
|
20
36
|
console.log("No news items.");
|
|
21
37
|
return;
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
console.log();
|
|
25
|
-
console.log(`\x1b[1m NEWS FEED\x1b[0m (${items.length})`);
|
|
26
|
-
console.log(` ${"─".repeat(
|
|
41
|
+
console.log(`\x1b[1m NEWS FEED\x1b[0m (showing ${items.length} of ${allItems.length})`);
|
|
42
|
+
console.log(` ${"─".repeat(74)}`);
|
|
27
43
|
for (const item of items) {
|
|
28
|
-
const title = truncate(item.title || item.headline || "?",
|
|
29
|
-
const source = item.source ?
|
|
30
|
-
|
|
44
|
+
const title = truncate(item.title || item.headline || "?", 50).padEnd(52);
|
|
45
|
+
const source = item.source ? truncate(item.source, 12).padStart(12) : "";
|
|
46
|
+
const ago = timeAgo(item.published_at || item.created_at || item.timestamp);
|
|
47
|
+
console.log(` ${title} \x1b[2m${source.padStart(12)}\x1b[0m \x1b[2m${ago}\x1b[0m`);
|
|
31
48
|
}
|
|
32
49
|
console.log();
|
|
33
50
|
}
|
package/lib/commands/nodes.mjs
CHANGED
|
@@ -1,88 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Federation node commands: nodes, msg,
|
|
2
|
+
* Federation node commands: nodes, msg, broadcast
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { get, post } from "../api.mjs";
|
|
6
6
|
import { hostname } from "node:os";
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Send a remote command to a node.
|
|
10
|
-
* Usage: cc cmd <node_id_or_name> <command> [...args]
|
|
11
|
-
* Commands: update, status, diagnose, restart, ping
|
|
12
|
-
*/
|
|
13
|
-
export async function sendCommand(args) {
|
|
14
|
-
if (args.length < 2) {
|
|
15
|
-
console.log("Usage: cc cmd <node_id_or_name> <command>");
|
|
16
|
-
console.log("Commands: update, status, diagnose, restart, ping");
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const [target, command, ...cmdArgs] = args;
|
|
21
|
-
|
|
22
|
-
// Resolve target: could be node_id prefix or hostname
|
|
23
|
-
const nodes = await get("/api/federation/nodes");
|
|
24
|
-
const node = nodes?.find(
|
|
25
|
-
(n) =>
|
|
26
|
-
n.node_id?.startsWith(target) ||
|
|
27
|
-
n.hostname?.toLowerCase().includes(target.toLowerCase()),
|
|
28
|
-
);
|
|
29
|
-
if (!node) {
|
|
30
|
-
console.log(`Node not found: ${target}`);
|
|
31
|
-
console.log("Available nodes:");
|
|
32
|
-
for (const n of nodes || []) {
|
|
33
|
-
console.log(` ${n.node_id?.slice(0, 12)} ${n.hostname}`);
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const myNodeId = nodes?.find(
|
|
39
|
-
(n) => n.hostname === hostname(),
|
|
40
|
-
)?.node_id || "unknown";
|
|
41
|
-
|
|
42
|
-
console.log(
|
|
43
|
-
`Sending \x1b[1m${command}\x1b[0m to \x1b[1m${node.hostname}\x1b[0m (${node.node_id?.slice(0, 12)})...`,
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const result = await post(`/api/federation/nodes/${myNodeId}/messages`, {
|
|
47
|
-
from_node: myNodeId,
|
|
48
|
-
to_node: node.node_id,
|
|
49
|
-
type: "command",
|
|
50
|
-
text: `Remote command: ${command} ${cmdArgs.join(" ")}`.trim(),
|
|
51
|
-
payload: { command, args: cmdArgs },
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (result?.id) {
|
|
55
|
-
const msgId = result.id;
|
|
56
|
-
console.log(`\x1b[32m✓\x1b[0m Command sent (msg ${msgId.slice(0, 12)})`);
|
|
57
|
-
console.log(" Waiting for reply (up to 3 min)...");
|
|
58
|
-
|
|
59
|
-
// Poll for reply
|
|
60
|
-
const deadline = Date.now() + 180_000;
|
|
61
|
-
const pollInterval = 10_000;
|
|
62
|
-
while (Date.now() < deadline) {
|
|
63
|
-
await new Promise((r) => setTimeout(r, pollInterval));
|
|
64
|
-
process.stdout.write(".");
|
|
65
|
-
|
|
66
|
-
const inbox = await get(
|
|
67
|
-
`/api/federation/nodes/${myNodeId}/messages?unread_only=false&limit=20`,
|
|
68
|
-
);
|
|
69
|
-
const reply = inbox?.messages?.find(
|
|
70
|
-
(m) =>
|
|
71
|
-
(m.type === "command_response" || m.type === "ack") &&
|
|
72
|
-
m.payload?.in_reply_to === msgId,
|
|
73
|
-
);
|
|
74
|
-
if (reply) {
|
|
75
|
-
console.log(`\n\x1b[32m✓\x1b[0m Reply from ${node.hostname}:`);
|
|
76
|
-
console.log(` ${reply.text}`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
console.log("\n\x1b[33m⏱\x1b[0m No reply within 3 min. Check later: cc inbox");
|
|
81
|
-
} else {
|
|
82
|
-
console.log("\x1b[31m✗\x1b[0m Failed to send command");
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
8
|
export async function listNodes() {
|
|
87
9
|
const nodes = await get("/api/federation/nodes");
|
|
88
10
|
if (!nodes || !Array.isArray(nodes)) {
|
|
@@ -94,6 +16,21 @@ export async function listNodes() {
|
|
|
94
16
|
console.log("\x1b[1m FEDERATION NODES\x1b[0m");
|
|
95
17
|
console.log(` ${"─".repeat(50)}`);
|
|
96
18
|
|
|
19
|
+
/** Format relative time */
|
|
20
|
+
function relativeTime(min) {
|
|
21
|
+
if (min < 1) return "just now";
|
|
22
|
+
if (min < 60) return `${min}m ago`;
|
|
23
|
+
if (min < 1440) return `${Math.floor(min / 60)}h ago`;
|
|
24
|
+
return `${Math.floor(min / 1440)}d ago`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Colored provider badge */
|
|
28
|
+
function providerBadge(name) {
|
|
29
|
+
const colors = { openrouter: "\x1b[36m", ollama: "\x1b[32m", anthropic: "\x1b[33m" };
|
|
30
|
+
const color = colors[name.toLowerCase()] || "\x1b[2m";
|
|
31
|
+
return `${color}[${name}]\x1b[0m`;
|
|
32
|
+
}
|
|
33
|
+
|
|
97
34
|
const now = Date.now();
|
|
98
35
|
for (const node of nodes) {
|
|
99
36
|
const lastSeen = node.last_seen_at ? new Date(node.last_seen_at) : null;
|
|
@@ -105,10 +42,6 @@ export async function listNodes() {
|
|
|
105
42
|
if (ageMin < 5) dot = "\x1b[32m●\x1b[0m"; // green
|
|
106
43
|
else if (ageMin < 60) dot = "\x1b[33m●\x1b[0m"; // yellow
|
|
107
44
|
|
|
108
|
-
// OS icon
|
|
109
|
-
const os = node.os_type || "?";
|
|
110
|
-
const icon = os === "macos" ? "🍎" : os === "windows" ? "🪟" : os === "linux" ? "🐧" : "🖥️";
|
|
111
|
-
|
|
112
45
|
// Providers
|
|
113
46
|
let providers = [];
|
|
114
47
|
try {
|
|
@@ -117,22 +50,17 @@ export async function listNodes() {
|
|
|
117
50
|
: (node.providers || []);
|
|
118
51
|
} catch { providers = []; }
|
|
119
52
|
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
const sha = git.local_sha || "?";
|
|
125
|
-
const originSha = git.origin_sha || "?";
|
|
126
|
-
const upToDate = git.up_to_date === "yes";
|
|
127
|
-
const versionTag = upToDate
|
|
128
|
-
? `\x1b[32m${sha}\x1b[0m`
|
|
129
|
-
: `\x1b[31m${sha} (origin: ${originSha})\x1b[0m`;
|
|
53
|
+
const shortId = (node.node_id || "").slice(0, 7);
|
|
54
|
+
const ago = relativeTime(ageMin);
|
|
55
|
+
const hostName = (node.hostname || "?").slice(0, 24);
|
|
56
|
+
const os = node.os_type || "?";
|
|
130
57
|
|
|
131
|
-
console.log(` ${dot} ${
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
58
|
+
console.log(` ${dot} \x1b[1m${hostName.padEnd(26)}\x1b[0m ${ago.padEnd(10)} \x1b[2m${shortId}\x1b[0m ${os}`);
|
|
59
|
+
if (providers.length > 0) {
|
|
60
|
+
console.log(` ${providers.map(providerBadge).join(" ")}`);
|
|
61
|
+
}
|
|
135
62
|
}
|
|
63
|
+
console.log();
|
|
136
64
|
}
|
|
137
65
|
|
|
138
66
|
export async function sendMessage(args) {
|
|
@@ -173,45 +101,15 @@ export async function sendMessage(args) {
|
|
|
173
101
|
console.log("\x1b[31m✗\x1b[0m Failed to broadcast");
|
|
174
102
|
}
|
|
175
103
|
} else {
|
|
176
|
-
// Resolve target name to node_id
|
|
177
|
-
const targetNode = Array.isArray(nodes)
|
|
178
|
-
? nodes.find(
|
|
179
|
-
(n) =>
|
|
180
|
-
n.node_id?.startsWith(targetOrBroadcast) ||
|
|
181
|
-
n.hostname?.toLowerCase().includes(targetOrBroadcast.toLowerCase()),
|
|
182
|
-
)
|
|
183
|
-
: null;
|
|
184
|
-
const toNodeId = targetNode?.node_id || targetOrBroadcast;
|
|
185
|
-
const toName = targetNode?.hostname || targetOrBroadcast.slice(0, 12);
|
|
186
|
-
|
|
187
104
|
const result = await post(`/api/federation/nodes/${myNodeId}/messages`, {
|
|
188
105
|
from_node: myNodeId,
|
|
189
|
-
to_node:
|
|
106
|
+
to_node: targetOrBroadcast,
|
|
190
107
|
type: "text",
|
|
191
108
|
text,
|
|
192
109
|
payload: {},
|
|
193
110
|
});
|
|
194
|
-
if (result
|
|
195
|
-
|
|
196
|
-
console.log(`\x1b[32m✓\x1b[0m Message sent to ${toName}: ${text.slice(0, 60)}`);
|
|
197
|
-
console.log(" Waiting for ack (up to 3 min)...");
|
|
198
|
-
|
|
199
|
-
const deadline = Date.now() + 180_000;
|
|
200
|
-
while (Date.now() < deadline) {
|
|
201
|
-
await new Promise((r) => setTimeout(r, 10_000));
|
|
202
|
-
process.stdout.write(".");
|
|
203
|
-
const inbox = await get(
|
|
204
|
-
`/api/federation/nodes/${myNodeId}/messages?unread_only=false&limit=20`,
|
|
205
|
-
);
|
|
206
|
-
const ack = inbox?.messages?.find(
|
|
207
|
-
(m) => m.type === "ack" && m.payload?.in_reply_to === msgId,
|
|
208
|
-
);
|
|
209
|
-
if (ack) {
|
|
210
|
-
console.log(`\n\x1b[32m✓\x1b[0m Acknowledged by ${toName}`);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
console.log("\n\x1b[33m⏱\x1b[0m No ack within 3 min. Node may be offline. Check: cc inbox");
|
|
111
|
+
if (result) {
|
|
112
|
+
console.log(`\x1b[32m✓\x1b[0m Message sent to ${targetOrBroadcast.slice(0, 12)}: ${text.slice(0, 60)}`);
|
|
215
113
|
} else {
|
|
216
114
|
console.log("\x1b[31m✗\x1b[0m Failed to send message");
|
|
217
115
|
}
|
|
@@ -4,9 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
import { get } from "../api.mjs";
|
|
6
6
|
|
|
7
|
+
/** Truncate at word boundary, append "..." if needed */
|
|
7
8
|
function truncate(str, len) {
|
|
8
9
|
if (!str) return "";
|
|
9
|
-
|
|
10
|
+
if (str.length <= len) return str;
|
|
11
|
+
const trimmed = str.slice(0, len - 3);
|
|
12
|
+
const lastSpace = trimmed.lastIndexOf(" ");
|
|
13
|
+
return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Mini bar for success rate */
|
|
17
|
+
function rateBar(rate, width = 10) {
|
|
18
|
+
const pct = Math.max(0, Math.min(1, rate));
|
|
19
|
+
const filled = Math.round(pct * width);
|
|
20
|
+
const color = pct >= 0.9 ? "\x1b[32m" : pct >= 0.7 ? "\x1b[33m" : "\x1b[31m";
|
|
21
|
+
return `${color}${"\u2593".repeat(filled)}${"\u2591".repeat(width - filled)}\x1b[0m`;
|
|
10
22
|
}
|
|
11
23
|
|
|
12
24
|
export async function listProviders() {
|
|
@@ -23,11 +35,22 @@ export async function listProviders() {
|
|
|
23
35
|
|
|
24
36
|
console.log();
|
|
25
37
|
console.log(`\x1b[1m PROVIDERS\x1b[0m (${data.length})`);
|
|
26
|
-
console.log(` ${"─".repeat(
|
|
38
|
+
console.log(` ${"─".repeat(68)}`);
|
|
27
39
|
for (const p of data) {
|
|
28
|
-
const name = truncate(p.name || p.id || "?",
|
|
29
|
-
const
|
|
30
|
-
|
|
40
|
+
const name = truncate(p.name || p.id || "?", 22).padEnd(24);
|
|
41
|
+
const successRate = p.success_rate ?? p.rate ?? null;
|
|
42
|
+
const samples = p.sample_count ?? p.count ?? p.total ?? null;
|
|
43
|
+
|
|
44
|
+
let rateStr = "";
|
|
45
|
+
if (successRate != null) {
|
|
46
|
+
const pct = (successRate * 100).toFixed(0);
|
|
47
|
+
rateStr = `${rateBar(successRate)} ${pct}%`.padEnd(22);
|
|
48
|
+
} else {
|
|
49
|
+
rateStr = "\x1b[2m-\x1b[0m".padEnd(22);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sampleStr = samples != null ? `\x1b[2m${String(samples).padStart(5)} samples\x1b[0m` : "";
|
|
53
|
+
console.log(` ${name} ${rateStr} ${sampleStr}`);
|
|
31
54
|
}
|
|
32
55
|
console.log();
|
|
33
56
|
}
|
|
@@ -41,16 +64,38 @@ export async function showProviderStats() {
|
|
|
41
64
|
|
|
42
65
|
console.log();
|
|
43
66
|
console.log(`\x1b[1m PROVIDER STATS\x1b[0m`);
|
|
44
|
-
console.log(` ${"─".repeat(
|
|
67
|
+
console.log(` ${"─".repeat(68)}`);
|
|
45
68
|
if (Array.isArray(data)) {
|
|
46
69
|
for (const p of data) {
|
|
47
|
-
const name = (p.name || p.provider || "?").padEnd(
|
|
48
|
-
const
|
|
49
|
-
|
|
70
|
+
const name = (p.name || p.provider || "?").padEnd(22);
|
|
71
|
+
const successRate = p.success_rate ?? p.rate ?? null;
|
|
72
|
+
const count = p.count ?? p.total ?? null;
|
|
73
|
+
|
|
74
|
+
let rateStr = "";
|
|
75
|
+
if (successRate != null) {
|
|
76
|
+
const pct = (successRate * 100).toFixed(0);
|
|
77
|
+
rateStr = `${rateBar(successRate)} ${pct}%`.padEnd(22);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const countStr = count != null ? `${String(count).padStart(6)} uses` : "";
|
|
81
|
+
console.log(` ${name} ${rateStr} ${countStr}`);
|
|
50
82
|
}
|
|
51
83
|
} else if (typeof data === "object") {
|
|
52
84
|
for (const [key, val] of Object.entries(data)) {
|
|
53
|
-
|
|
85
|
+
const name = key.padEnd(22);
|
|
86
|
+
if (typeof val === "object" && val != null) {
|
|
87
|
+
const successRate = val.success_rate ?? val.rate ?? null;
|
|
88
|
+
const count = val.count ?? val.total ?? null;
|
|
89
|
+
let rateStr = "";
|
|
90
|
+
if (successRate != null) {
|
|
91
|
+
const pct = (successRate * 100).toFixed(0);
|
|
92
|
+
rateStr = `${rateBar(successRate)} ${pct}%`.padEnd(22);
|
|
93
|
+
}
|
|
94
|
+
const countStr = count != null ? `${String(count).padStart(6)} uses` : "";
|
|
95
|
+
console.log(` ${name} ${rateStr} ${countStr}`);
|
|
96
|
+
} else {
|
|
97
|
+
console.log(` ${name} ${val}`);
|
|
98
|
+
}
|
|
54
99
|
}
|
|
55
100
|
}
|
|
56
101
|
console.log();
|
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { get } from "../api.mjs";
|
|
6
6
|
|
|
7
|
+
/** Truncate at word boundary, append "..." if needed */
|
|
7
8
|
function truncate(str, len) {
|
|
8
9
|
if (!str) return "";
|
|
9
|
-
|
|
10
|
+
if (str.length <= len) return str;
|
|
11
|
+
const trimmed = str.slice(0, len - 3);
|
|
12
|
+
const lastSpace = trimmed.lastIndexOf(" ");
|
|
13
|
+
return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export async function listServices() {
|
|
@@ -23,12 +27,16 @@ export async function listServices() {
|
|
|
23
27
|
|
|
24
28
|
console.log();
|
|
25
29
|
console.log(`\x1b[1m SERVICES\x1b[0m (${data.length})`);
|
|
26
|
-
console.log(` ${"─".repeat(
|
|
30
|
+
console.log(` ${"─".repeat(68)}`);
|
|
27
31
|
for (const s of data) {
|
|
28
|
-
const name = truncate(s.name || s.id, 30);
|
|
29
|
-
const status = s.status || "?";
|
|
30
|
-
const dot = status === "healthy" || status === "up" ? "\x1b[32m●\x1b[0m"
|
|
31
|
-
|
|
32
|
+
const name = truncate(s.name || s.id, 28).padEnd(30);
|
|
33
|
+
const status = (s.status || "?").toLowerCase();
|
|
34
|
+
const dot = status === "healthy" || status === "up" ? "\x1b[32m●\x1b[0m"
|
|
35
|
+
: status === "degraded" ? "\x1b[33m●\x1b[0m"
|
|
36
|
+
: "\x1b[31m●\x1b[0m";
|
|
37
|
+
const ver = s.version ? `\x1b[2mv${s.version}\x1b[0m`.padEnd(20) : "\x1b[2m-\x1b[0m".padEnd(20);
|
|
38
|
+
const caps = s.capabilities ? `${Array.isArray(s.capabilities) ? s.capabilities.length : 0} caps` : "";
|
|
39
|
+
console.log(` ${dot} ${name} ${ver} ${caps}`);
|
|
32
40
|
}
|
|
33
41
|
console.log();
|
|
34
42
|
}
|
package/lib/commands/specs.mjs
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import { get } from "../api.mjs";
|
|
6
6
|
|
|
7
|
+
/** Truncate at word boundary, append "..." if needed */
|
|
7
8
|
function truncate(str, len) {
|
|
8
9
|
if (!str) return "";
|
|
9
|
-
|
|
10
|
+
if (str.length <= len) return str;
|
|
11
|
+
const trimmed = str.slice(0, len - 3);
|
|
12
|
+
const lastSpace = trimmed.lastIndexOf(" ");
|
|
13
|
+
return (lastSpace > len * 0.4 ? trimmed.slice(0, lastSpace) : trimmed) + "...";
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export async function listSpecs(args) {
|
|
@@ -23,11 +27,15 @@ export async function listSpecs(args) {
|
|
|
23
27
|
|
|
24
28
|
console.log();
|
|
25
29
|
console.log(`\x1b[1m SPECS\x1b[0m (${data.length})`);
|
|
26
|
-
console.log(` ${"─".repeat(
|
|
30
|
+
console.log(` ${"─".repeat(74)}`);
|
|
31
|
+
console.log(`\x1b[2m ${"Spec ID".padEnd(22)} ${"Title".padEnd(37)} ${"ROI".padStart(6)} ${"Gap".padStart(5)}\x1b[0m`);
|
|
32
|
+
console.log(` ${"─".repeat(74)}`);
|
|
27
33
|
for (const s of data) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
34
|
+
const specId = truncate(s.spec_id || "", 20).padEnd(22);
|
|
35
|
+
const title = truncate(s.title || "", 35).padEnd(37);
|
|
36
|
+
const roi = s.estimated_roi != null ? String(s.estimated_roi.toFixed(1)).padStart(6) : " -";
|
|
37
|
+
const gap = s.value_gap != null ? String(s.value_gap.toFixed(0)).padStart(5) : " -";
|
|
38
|
+
console.log(` ${specId} ${title} ${roi} ${gap}`);
|
|
31
39
|
}
|
|
32
40
|
console.log();
|
|
33
41
|
}
|
package/lib/commands/status.mjs
CHANGED
|
@@ -5,6 +5,31 @@
|
|
|
5
5
|
import { get } from "../api.mjs";
|
|
6
6
|
import { getContributorId, getHubUrl } from "../config.mjs";
|
|
7
7
|
import { hostname } from "node:os";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import { stdin, stdout } from "node:process";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
function getLocalVersion() {
|
|
18
|
+
try {
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf8"));
|
|
20
|
+
return pkg.version;
|
|
21
|
+
} catch { return "?"; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function checkForUpdate(localVersion) {
|
|
25
|
+
try {
|
|
26
|
+
const latest = execSync("npm view coherence-cli version", { encoding: "utf8", timeout: 5000 }).trim();
|
|
27
|
+
if (latest && latest !== localVersion && localVersion !== "?") {
|
|
28
|
+
return latest;
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
8
33
|
|
|
9
34
|
export async function showStatus() {
|
|
10
35
|
// Fetch everything in parallel
|
|
@@ -66,7 +91,7 @@ export async function showStatus() {
|
|
|
66
91
|
|
|
67
92
|
console.log(` Hub: ${getHubUrl()}`);
|
|
68
93
|
console.log(` Node: ${hostname()}`);
|
|
69
|
-
console.log(` Identity: ${getContributorId() || "\x1b[
|
|
94
|
+
console.log(` Identity: ${getContributorId() || "\x1b[2m(anonymous — link with: cc identity link github <handle>)\x1b[0m"}`);
|
|
70
95
|
|
|
71
96
|
// Ideas
|
|
72
97
|
if (ideas) {
|
|
@@ -140,6 +165,40 @@ export async function showStatus() {
|
|
|
140
165
|
}
|
|
141
166
|
}
|
|
142
167
|
|
|
168
|
+
// Version check + auto-update prompt
|
|
169
|
+
const localVersion = getLocalVersion();
|
|
170
|
+
const latestVersion = await checkForUpdate(localVersion);
|
|
171
|
+
if (latestVersion) {
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(`\x1b[33m Update available: ${localVersion} → ${latestVersion}\x1b[0m`);
|
|
174
|
+
|
|
175
|
+
if (stdin.isTTY) {
|
|
176
|
+
// Interactive — ask
|
|
177
|
+
const { createInterface } = await import("node:readline/promises");
|
|
178
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
179
|
+
const answer = await rl.question(" Update now? (Y/n): ");
|
|
180
|
+
rl.close();
|
|
181
|
+
if (!answer || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
|
|
182
|
+
console.log(` Updating to ${latestVersion}...`);
|
|
183
|
+
try {
|
|
184
|
+
execSync("npm i -g coherence-cli@latest", { stdio: "inherit", timeout: 60000 });
|
|
185
|
+
console.log(`\x1b[32m ✓ Updated to ${latestVersion}\x1b[0m`);
|
|
186
|
+
} catch {
|
|
187
|
+
console.log(`\x1b[31m ✗ Update failed. Run manually: npm i -g coherence-cli@latest\x1b[0m`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// Non-interactive — auto-update silently
|
|
192
|
+
console.log(` Auto-updating to ${latestVersion}...`);
|
|
193
|
+
try {
|
|
194
|
+
execSync("npm i -g coherence-cli@latest", { stdio: "pipe", timeout: 60000 });
|
|
195
|
+
console.log(`\x1b[32m ✓ Updated to ${latestVersion}\x1b[0m`);
|
|
196
|
+
} catch {
|
|
197
|
+
console.log(`\x1b[2m Auto-update failed. Run: npm i -g coherence-cli@latest\x1b[0m`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
143
202
|
console.log();
|
|
144
203
|
}
|
|
145
204
|
|