@neta-art/cohub-cli 6.7.0 → 6.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/bootstrap.js +10 -0
- package/dist/commands/apps.js +19 -17
- package/dist/commands/auth.js +2 -0
- package/dist/commands/boards/context.js +1 -1
- package/dist/commands/boards.js +9 -9
- package/dist/commands/desktop.js +1 -0
- package/dist/commands/generations.js +1 -1
- package/dist/commands/public.js +19 -16
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +7 -7
- package/dist/commands/sandbox.js +16 -12
- package/dist/commands/sandboxd-binary.js +1 -1
- package/dist/commands/space-activity.js +1 -1
- package/dist/commands/space-commerce.js +15 -15
- package/dist/commands/space-invitations.js +3 -3
- package/dist/commands/space-turns.js +4 -4
- package/dist/commands/spaces.js +63 -66
- package/dist/env-proxy.d.ts +5 -0
- package/dist/env-proxy.js +20 -0
- package/dist/help-path.d.ts +18 -0
- package/dist/help-path.js +223 -0
- package/dist/http-put.d.ts +25 -0
- package/dist/http-put.js +132 -0
- package/dist/index.js +30 -7
- package/dist/launcher.d.ts +3 -1
- package/dist/launcher.js +2 -2
- package/dist/space.d.ts +31 -1
- package/dist/space.js +117 -6
- package/package.json +2 -2
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
function isHelpFlag(token) {
|
|
2
|
+
return token === "-h" || token === "--help";
|
|
3
|
+
}
|
|
4
|
+
function findChild(command, name) {
|
|
5
|
+
return command.commands.find((child) => {
|
|
6
|
+
if (child.name() === "help")
|
|
7
|
+
return false;
|
|
8
|
+
return child.name() === name || child.aliases().includes(name);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function hasSubcommands(command) {
|
|
12
|
+
return command.commands.some((child) => child.name() !== "help");
|
|
13
|
+
}
|
|
14
|
+
function knownOptions(command) {
|
|
15
|
+
const options = [];
|
|
16
|
+
let current = command;
|
|
17
|
+
while (current) {
|
|
18
|
+
options.push(...current.options);
|
|
19
|
+
current = current.parent;
|
|
20
|
+
}
|
|
21
|
+
return options;
|
|
22
|
+
}
|
|
23
|
+
function matchOption(command, token) {
|
|
24
|
+
if (!token.startsWith("-") || token === "-")
|
|
25
|
+
return undefined;
|
|
26
|
+
const eq = token.indexOf("=");
|
|
27
|
+
const inline = eq >= 0;
|
|
28
|
+
const flag = inline ? token.slice(0, eq) : token;
|
|
29
|
+
for (const option of knownOptions(command)) {
|
|
30
|
+
if (option.short === flag || option.long === flag)
|
|
31
|
+
return { option, inline };
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function skipOption(command, argv, index) {
|
|
36
|
+
const token = argv[index] ?? "";
|
|
37
|
+
const matched = matchOption(command, token);
|
|
38
|
+
const next = index + 1;
|
|
39
|
+
if (matched?.inline)
|
|
40
|
+
return next;
|
|
41
|
+
const value = argv[next];
|
|
42
|
+
if (!value)
|
|
43
|
+
return next;
|
|
44
|
+
if (matched?.option.required)
|
|
45
|
+
return next + 1;
|
|
46
|
+
if (matched?.option.optional && !value.startsWith("-"))
|
|
47
|
+
return next + 1;
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
function remainderRequestsHelp(command, argv) {
|
|
51
|
+
let index = 0;
|
|
52
|
+
while (index < argv.length) {
|
|
53
|
+
const token = argv[index] ?? "";
|
|
54
|
+
if (token === "--")
|
|
55
|
+
break;
|
|
56
|
+
if (isHelpFlag(token))
|
|
57
|
+
return true;
|
|
58
|
+
if (token.startsWith("-")) {
|
|
59
|
+
index = skipOption(command, argv, index);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
index += 1;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/** Match `run.ts`: Cohub flags first, then the rest is the shell command. */
|
|
67
|
+
function runRemainderRequestsHelp(argv) {
|
|
68
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
69
|
+
const token = argv[index] ?? "";
|
|
70
|
+
if (token === "--")
|
|
71
|
+
return false;
|
|
72
|
+
if (isHelpFlag(token))
|
|
73
|
+
return true;
|
|
74
|
+
if (token === "--async" || token === "--json")
|
|
75
|
+
continue;
|
|
76
|
+
if (token === "-c" || token === "--command") {
|
|
77
|
+
index += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (token.startsWith("--command="))
|
|
81
|
+
continue;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function walkCommandPath(program, argv) {
|
|
87
|
+
let command = program;
|
|
88
|
+
const path = [];
|
|
89
|
+
let helpRequested = false;
|
|
90
|
+
let index = 0;
|
|
91
|
+
while (index < argv.length) {
|
|
92
|
+
const token = argv[index] ?? "";
|
|
93
|
+
if (token === "--")
|
|
94
|
+
break;
|
|
95
|
+
if (isHelpFlag(token)) {
|
|
96
|
+
helpRequested = true;
|
|
97
|
+
index += 1;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (token.startsWith("-")) {
|
|
101
|
+
index = skipOption(command, argv, index);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (token === "help" && hasSubcommands(command)) {
|
|
105
|
+
helpRequested = true;
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const child = findChild(command, token);
|
|
110
|
+
if (child) {
|
|
111
|
+
command = child;
|
|
112
|
+
path.push(child.name());
|
|
113
|
+
index += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (hasSubcommands(command)) {
|
|
117
|
+
const remainder = argv.slice(index + 1);
|
|
118
|
+
const helpAfter = token === "run" && path.length === 0
|
|
119
|
+
? runRemainderRequestsHelp(remainder)
|
|
120
|
+
: remainderRequestsHelp(command, remainder);
|
|
121
|
+
return {
|
|
122
|
+
command,
|
|
123
|
+
path,
|
|
124
|
+
helpRequested: helpRequested || helpAfter,
|
|
125
|
+
unknown: token,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
return { command, path, helpRequested, unknown: null };
|
|
131
|
+
}
|
|
132
|
+
function stripRootCommand(program, argv, name) {
|
|
133
|
+
let index = 0;
|
|
134
|
+
while (index < argv.length) {
|
|
135
|
+
const token = argv[index] ?? "";
|
|
136
|
+
if (token === "--")
|
|
137
|
+
return undefined;
|
|
138
|
+
if (isHelpFlag(token) || token === "help") {
|
|
139
|
+
index += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (token.startsWith("-")) {
|
|
143
|
+
index = skipOption(program, argv, index);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (token !== name)
|
|
147
|
+
return undefined;
|
|
148
|
+
return [...argv.slice(0, index), ...argv.slice(index + 1)];
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
function editDistance(a, b) {
|
|
153
|
+
if (a === b)
|
|
154
|
+
return 0;
|
|
155
|
+
if (Math.abs(a.length - b.length) > 3)
|
|
156
|
+
return Math.max(a.length, b.length);
|
|
157
|
+
const rows = a.length + 1;
|
|
158
|
+
const cols = b.length + 1;
|
|
159
|
+
const previous = Array.from({ length: cols }, (_, index) => index);
|
|
160
|
+
const current = new Array(cols);
|
|
161
|
+
for (let i = 1; i < rows; i += 1) {
|
|
162
|
+
current[0] = i;
|
|
163
|
+
for (let j = 1; j < cols; j += 1) {
|
|
164
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
165
|
+
current[j] = Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
|
|
166
|
+
}
|
|
167
|
+
for (let j = 0; j < cols; j += 1)
|
|
168
|
+
previous[j] = current[j] ?? 0;
|
|
169
|
+
}
|
|
170
|
+
return previous[b.length] ?? Math.max(a.length, b.length);
|
|
171
|
+
}
|
|
172
|
+
function suggestCommand(command, token) {
|
|
173
|
+
const names = [...new Set(command.commands.flatMap((child) => {
|
|
174
|
+
if (child.name() === "help" || child.name().length <= 1)
|
|
175
|
+
return [];
|
|
176
|
+
return [child.name(), ...child.aliases().filter((alias) => alias.length > 1)];
|
|
177
|
+
}))];
|
|
178
|
+
let best;
|
|
179
|
+
let bestDistance = 3;
|
|
180
|
+
for (const name of names) {
|
|
181
|
+
const distance = editDistance(token, name);
|
|
182
|
+
const length = Math.max(token.length, name.length);
|
|
183
|
+
if (distance >= bestDistance || (length - distance) / length <= 0.4)
|
|
184
|
+
continue;
|
|
185
|
+
bestDistance = distance;
|
|
186
|
+
best = name;
|
|
187
|
+
}
|
|
188
|
+
return best;
|
|
189
|
+
}
|
|
190
|
+
export function resolveHelpPath(program, argv) {
|
|
191
|
+
const walk = walkCommandPath(program, argv);
|
|
192
|
+
const cliAtRoot = walk.unknown === "cli" && walk.path.length === 0;
|
|
193
|
+
if (cliAtRoot) {
|
|
194
|
+
const stripped = stripRootCommand(program, argv, "cli");
|
|
195
|
+
return {
|
|
196
|
+
kind: "unknown",
|
|
197
|
+
token: "cli",
|
|
198
|
+
tryCommand: stripped && stripped.length > 0 ? stripped.join(" ") : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (!walk.helpRequested)
|
|
202
|
+
return { kind: "passthrough" };
|
|
203
|
+
if (walk.unknown === "run" && walk.path.length === 0)
|
|
204
|
+
return { kind: "run-help" };
|
|
205
|
+
if (walk.unknown) {
|
|
206
|
+
return {
|
|
207
|
+
kind: "unknown",
|
|
208
|
+
token: walk.unknown,
|
|
209
|
+
suggestion: suggestCommand(walk.command, walk.unknown),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { kind: "help", command: walk.command };
|
|
213
|
+
}
|
|
214
|
+
export function formatUnknownCommandError(result) {
|
|
215
|
+
const lines = [`error: unknown command '${result.token}'`];
|
|
216
|
+
if (result.tryCommand)
|
|
217
|
+
lines.push(`Try: cohub ${result.tryCommand}`);
|
|
218
|
+
else if (result.token === "cli")
|
|
219
|
+
lines.push(`cohub has no "cli" subcommand. See cohub --help`);
|
|
220
|
+
else if (result.suggestion)
|
|
221
|
+
lines.push(`(Did you mean ${result.suggestion}?)`);
|
|
222
|
+
return `${lines.join("\n")}\n`;
|
|
223
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class HttpPutError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly body: string;
|
|
4
|
+
constructor(message: string, status: number, body?: string);
|
|
5
|
+
}
|
|
6
|
+
export type PutRetryOptions = {
|
|
7
|
+
attempts?: number;
|
|
8
|
+
delayMs?: number;
|
|
9
|
+
sleep?: (ms: number) => Promise<void>;
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
};
|
|
12
|
+
export declare function putLocalFile(input: PutRetryOptions & {
|
|
13
|
+
url: string;
|
|
14
|
+
filePath: string;
|
|
15
|
+
size: number;
|
|
16
|
+
headers?: HeadersInit;
|
|
17
|
+
label: string;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
export declare function putBytes(input: PutRetryOptions & {
|
|
20
|
+
url: string;
|
|
21
|
+
body: Blob;
|
|
22
|
+
size?: number;
|
|
23
|
+
headers?: HeadersInit;
|
|
24
|
+
label: string;
|
|
25
|
+
}): Promise<void>;
|
package/dist/http-put.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
const DEFAULT_ATTEMPTS = 3;
|
|
3
|
+
const DEFAULT_DELAY_MS = 200;
|
|
4
|
+
const RETRYABLE_STATUS = new Set([408, 411, 425, 429, 500, 502, 503, 504]);
|
|
5
|
+
const RETRYABLE_CODES = new Set([
|
|
6
|
+
"BodyTimeoutError",
|
|
7
|
+
"ConnectTimeoutError",
|
|
8
|
+
"EAI_AGAIN",
|
|
9
|
+
"ECONNREFUSED",
|
|
10
|
+
"ECONNRESET",
|
|
11
|
+
"EHOSTUNREACH",
|
|
12
|
+
"ENETUNREACH",
|
|
13
|
+
"ENOTFOUND",
|
|
14
|
+
"EPIPE",
|
|
15
|
+
"ETIMEDOUT",
|
|
16
|
+
"HeadersTimeoutError",
|
|
17
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
18
|
+
"UND_ERR_CONNECT",
|
|
19
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
20
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
21
|
+
"UND_ERR_SOCKET",
|
|
22
|
+
]);
|
|
23
|
+
export class HttpPutError extends Error {
|
|
24
|
+
status;
|
|
25
|
+
body;
|
|
26
|
+
constructor(message, status, body = "") {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "HttpPutError";
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.body = body;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function withContentLength(headers, contentLength) {
|
|
34
|
+
const result = new Headers(headers);
|
|
35
|
+
if (contentLength !== undefined && !result.has("content-length")) {
|
|
36
|
+
result.set("content-length", String(contentLength));
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function errorCode(error) {
|
|
41
|
+
if (!error || typeof error !== "object")
|
|
42
|
+
return undefined;
|
|
43
|
+
const record = error;
|
|
44
|
+
if (typeof record.code === "string")
|
|
45
|
+
return record.code;
|
|
46
|
+
if (typeof record.cause?.code === "string")
|
|
47
|
+
return record.cause.code;
|
|
48
|
+
if (typeof record.cause?.name === "string")
|
|
49
|
+
return record.cause.name;
|
|
50
|
+
if (typeof record.name === "string")
|
|
51
|
+
return record.name;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
function isRetryableError(error) {
|
|
55
|
+
const code = errorCode(error);
|
|
56
|
+
return Boolean(code && RETRYABLE_CODES.has(code));
|
|
57
|
+
}
|
|
58
|
+
function isRetryableStatus(status) {
|
|
59
|
+
return RETRYABLE_STATUS.has(status);
|
|
60
|
+
}
|
|
61
|
+
function defaultSleep(ms) {
|
|
62
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
64
|
+
function failureMessage(label, status, detail) {
|
|
65
|
+
return `Failed to upload ${label}: HTTP ${status}${detail ? ` — ${detail}` : ""}`;
|
|
66
|
+
}
|
|
67
|
+
async function putWithRetry(input) {
|
|
68
|
+
const attempts = input.attempts ?? DEFAULT_ATTEMPTS;
|
|
69
|
+
const delayMs = input.delayMs ?? DEFAULT_DELAY_MS;
|
|
70
|
+
const sleep = input.sleep ?? defaultSleep;
|
|
71
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
72
|
+
let lastError;
|
|
73
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetchImpl(input.url, {
|
|
76
|
+
method: "PUT",
|
|
77
|
+
headers: withContentLength(input.headers, input.contentLength),
|
|
78
|
+
body: input.body(),
|
|
79
|
+
...(input.duplex ? { duplex: "half" } : {}),
|
|
80
|
+
});
|
|
81
|
+
if (response.ok)
|
|
82
|
+
return response;
|
|
83
|
+
const detail = await response.text().catch(() => "");
|
|
84
|
+
const error = new HttpPutError(failureMessage(input.label, response.status, detail), response.status, detail);
|
|
85
|
+
if (attempt < attempts - 1 && isRetryableStatus(response.status)) {
|
|
86
|
+
lastError = error;
|
|
87
|
+
await sleep(delayMs * 2 ** attempt);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof HttpPutError)
|
|
94
|
+
throw error;
|
|
95
|
+
lastError = error;
|
|
96
|
+
if (attempt < attempts - 1 && isRetryableError(error)) {
|
|
97
|
+
await sleep(delayMs * 2 ** attempt);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw lastError;
|
|
104
|
+
}
|
|
105
|
+
export async function putLocalFile(input) {
|
|
106
|
+
await putWithRetry({
|
|
107
|
+
url: input.url,
|
|
108
|
+
body: () => createReadStream(input.filePath),
|
|
109
|
+
headers: input.headers,
|
|
110
|
+
contentLength: input.size,
|
|
111
|
+
duplex: true,
|
|
112
|
+
label: input.label,
|
|
113
|
+
attempts: input.attempts,
|
|
114
|
+
delayMs: input.delayMs,
|
|
115
|
+
sleep: input.sleep,
|
|
116
|
+
fetch: input.fetch,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
export async function putBytes(input) {
|
|
120
|
+
const body = input.body;
|
|
121
|
+
await putWithRetry({
|
|
122
|
+
url: input.url,
|
|
123
|
+
body: () => body,
|
|
124
|
+
headers: input.headers,
|
|
125
|
+
contentLength: input.size,
|
|
126
|
+
label: input.label,
|
|
127
|
+
attempts: input.attempts,
|
|
128
|
+
delayMs: input.delayMs,
|
|
129
|
+
sleep: input.sleep,
|
|
130
|
+
fetch: input.fetch,
|
|
131
|
+
});
|
|
132
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -16,11 +16,12 @@ import { registerSearch } from "./commands/search.js";
|
|
|
16
16
|
import { registerReferences } from "./commands/references.js";
|
|
17
17
|
import { registerReferrals } from "./commands/referrals.js";
|
|
18
18
|
import { registerPrompt, registerSpaces } from "./commands/spaces.js";
|
|
19
|
-
import { maybeHandleRunCommand } from "./commands/run.js";
|
|
19
|
+
import { maybeHandleRunCommand, printRunHelp } from "./commands/run.js";
|
|
20
20
|
import { registerSandbox } from "./commands/sandbox.js";
|
|
21
21
|
import { registerTasks } from "./commands/tasks.js";
|
|
22
22
|
import { registerDesktop, registerLegacyUi } from "./commands/desktop.js";
|
|
23
23
|
import { registerApps } from "./commands/apps.js";
|
|
24
|
+
import { formatUnknownCommandError, resolveHelpPath } from "./help-path.js";
|
|
24
25
|
const VERSION = (() => {
|
|
25
26
|
try {
|
|
26
27
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
@@ -36,18 +37,22 @@ program
|
|
|
36
37
|
.summary("Work with Cohub from your terminal")
|
|
37
38
|
.description("Send prompts, manage Space files, and publish public output.")
|
|
38
39
|
.version(VERSION, "-v, --version", "Show version")
|
|
39
|
-
.option("-s, --space <id>", "Target Space ID")
|
|
40
|
+
.option("-s, --space <id>", "Target Space ID (defaults to your Home space)")
|
|
40
41
|
.option("--json", "Print machine-readable JSON when supported")
|
|
41
42
|
.helpOption("-h, --help", "Show help")
|
|
42
43
|
.addHelpText("after", `
|
|
43
44
|
|
|
45
|
+
Help:
|
|
46
|
+
cohub apps publish --help
|
|
47
|
+
cohub help apps publish
|
|
48
|
+
|
|
44
49
|
Common commands:
|
|
45
50
|
cohub auth login
|
|
46
51
|
cohub profile avatar ./avatar.png
|
|
47
52
|
cohub spaces ls
|
|
48
|
-
cohub
|
|
49
|
-
cohub
|
|
50
|
-
cohub
|
|
53
|
+
cohub prompt "Fix the failing tests"
|
|
54
|
+
cohub completion "Summarize AGENTS.md" --system-prompt AGENTS.md --stream
|
|
55
|
+
cohub run -- git status
|
|
51
56
|
cohub sandbox up ./my-project
|
|
52
57
|
cohub search "release notes"
|
|
53
58
|
cohub -s <space-id> boards inspect <board-id>
|
|
@@ -62,9 +67,14 @@ Common commands:
|
|
|
62
67
|
cohub models ls --model-type multimodal
|
|
63
68
|
cohub generate "A calm lake at sunrise" --model <model> --output lake.png
|
|
64
69
|
|
|
70
|
+
Target space:
|
|
71
|
+
-s <space-id>, then COHUB_SPACE_ID, then your Home space
|
|
72
|
+
|
|
65
73
|
Environment:
|
|
74
|
+
COHUB_SPACE_ID Target Space ID when -s is omitted
|
|
66
75
|
COHUB_EXECUTION_TOKEN Use this token instead of the stored Logto session
|
|
67
76
|
ENV=dev Use the development Cohub environment
|
|
77
|
+
HTTPS_PROXY Honored for API and uploads (also HTTP_PROXY, NO_PROXY)
|
|
68
78
|
`);
|
|
69
79
|
registerAuth(program);
|
|
70
80
|
registerBoards(program);
|
|
@@ -88,7 +98,20 @@ registerApps(program);
|
|
|
88
98
|
registerDesktop(program);
|
|
89
99
|
registerLegacyUi(program);
|
|
90
100
|
const argv = process.argv.slice(2);
|
|
91
|
-
|
|
101
|
+
const help = resolveHelpPath(program, argv);
|
|
102
|
+
if (help.kind === "help") {
|
|
103
|
+
help.command.outputHelp();
|
|
104
|
+
}
|
|
105
|
+
else if (help.kind === "run-help") {
|
|
106
|
+
printRunHelp();
|
|
107
|
+
}
|
|
108
|
+
else if (help.kind === "unknown") {
|
|
109
|
+
process.stderr.write(formatUnknownCommandError(help));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
else if (await maybeHandleRunCommand(argv)) {
|
|
92
113
|
process.exit();
|
|
93
114
|
}
|
|
94
|
-
|
|
115
|
+
else {
|
|
116
|
+
program.parse();
|
|
117
|
+
}
|
package/dist/launcher.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function exitCodeForChild(code: number | null, signal: NodeJS.Signals | null): number;
|
|
2
|
-
export declare function relaunchCli(entrypoint: string, argv: string[]
|
|
2
|
+
export declare function relaunchCli(entrypoint: string, argv: string[], options?: {
|
|
3
|
+
execArgv?: readonly string[];
|
|
4
|
+
}): Promise<number>;
|
package/dist/launcher.js
CHANGED
|
@@ -8,9 +8,9 @@ export function exitCodeForChild(code, signal) {
|
|
|
8
8
|
return 1;
|
|
9
9
|
return 128 + (constants.signals[signal] ?? 1);
|
|
10
10
|
}
|
|
11
|
-
export function relaunchCli(entrypoint, argv) {
|
|
11
|
+
export function relaunchCli(entrypoint, argv, options) {
|
|
12
12
|
return new Promise((resolve, reject) => {
|
|
13
|
-
const child = spawn(process.execPath, [entrypoint, ...argv], {
|
|
13
|
+
const child = spawn(process.execPath, [...(options?.execArgv ?? []), entrypoint, ...argv], {
|
|
14
14
|
env: process.env,
|
|
15
15
|
stdio: "inherit",
|
|
16
16
|
});
|
package/dist/space.d.ts
CHANGED
|
@@ -1,2 +1,32 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Cache key aligned with auth: execution token is exclusive (same as
|
|
4
|
+
* `resolveAccessToken`) and never falls back to a local Logto session.
|
|
5
|
+
* Execution grants identify the actor as `actorUserId`, not `sub`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function identityKeyFrom(input: {
|
|
8
|
+
env: string;
|
|
9
|
+
executionToken?: string | null;
|
|
10
|
+
idToken?: string | null;
|
|
11
|
+
accessToken?: string | null;
|
|
12
|
+
}): string | null;
|
|
13
|
+
/** Exported for tests; production always uses `CACHE_PATH`. */
|
|
14
|
+
export declare function readDefaultSpaceCache(path: string, key: string, now?: number): string | null;
|
|
15
|
+
export declare function clearDefaultSpaceCache(): void;
|
|
16
|
+
/** Explicit target from `-s/--space` (any ancestor) or `COHUB_SPACE_ID`, else null. */
|
|
17
|
+
export declare function explicitSpace(program: Command): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the user's home space when no target is given. Cached locally per
|
|
20
|
+
* identity so repeated invocations skip the network entirely, and memoized
|
|
21
|
+
* in-process so preAction hooks and actions share a single lookup.
|
|
22
|
+
* Network and auth failures propagate so callers can report them faithfully.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveDefaultSpace(): Promise<string | null>;
|
|
25
|
+
/** Shared exit for commands that need a space but resolved none. */
|
|
26
|
+
export declare function missingSpaceError(): never;
|
|
27
|
+
/**
|
|
28
|
+
* Target space for a command: explicit `-s`/`COHUB_SPACE_ID` first, then the
|
|
29
|
+
* user's home space. Exits with guidance when neither is available; request
|
|
30
|
+
* failures go through the shared HTTP error handler.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveSpace(program: Command): Promise<string>;
|
package/dist/space.js
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveCohubEnvironment } from "@neta-art/cohub";
|
|
5
|
+
import { readAuthSession } from "./auth.js";
|
|
6
|
+
import { createClient } from "./client.js";
|
|
7
|
+
import { error, handleHttp } from "./output.js";
|
|
8
|
+
const CONFIG_DIR = join(homedir(), ".config", "cohub");
|
|
9
|
+
const CACHE_PATH = join(CONFIG_DIR, "default-space.json");
|
|
10
|
+
/** Home space is stable; a one-day TTL bounds how long a stale hit survives. */
|
|
11
|
+
const CACHE_TTL_MS = 86_400_000;
|
|
12
|
+
function jwtClaim(token, key) {
|
|
13
|
+
const payload = token?.split(".")[1];
|
|
14
|
+
if (!payload)
|
|
15
|
+
return null;
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf-8"));
|
|
18
|
+
const value = parsed[key];
|
|
19
|
+
return typeof value === "string" && value ? value : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Cache key aligned with auth: execution token is exclusive (same as
|
|
27
|
+
* `resolveAccessToken`) and never falls back to a local Logto session.
|
|
28
|
+
* Execution grants identify the actor as `actorUserId`, not `sub`.
|
|
29
|
+
*/
|
|
30
|
+
export function identityKeyFrom(input) {
|
|
31
|
+
if (input.executionToken) {
|
|
32
|
+
const actor = jwtClaim(input.executionToken, "actorUserId") ?? jwtClaim(input.executionToken, "sub");
|
|
33
|
+
return actor ? `${input.env}:${actor}` : null;
|
|
34
|
+
}
|
|
35
|
+
const sub = jwtClaim(input.idToken, "sub") ?? jwtClaim(input.accessToken, "sub");
|
|
36
|
+
return sub ? `${input.env}:${sub}` : null;
|
|
37
|
+
}
|
|
38
|
+
function identityKey() {
|
|
39
|
+
const session = readAuthSession();
|
|
40
|
+
return identityKeyFrom({
|
|
41
|
+
env: resolveCohubEnvironment(),
|
|
42
|
+
executionToken: process.env.COHUB_EXECUTION_TOKEN?.trim(),
|
|
43
|
+
idToken: session?.idToken,
|
|
44
|
+
accessToken: session?.accessToken,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Exported for tests; production always uses `CACHE_PATH`. */
|
|
48
|
+
export function readDefaultSpaceCache(path, key, now = Date.now()) {
|
|
49
|
+
try {
|
|
50
|
+
const cache = JSON.parse(readFileSync(path, "utf-8"));
|
|
51
|
+
if (cache.key !== key || typeof cache.spaceId !== "string" || typeof cache.cachedAt !== "number")
|
|
52
|
+
return null;
|
|
53
|
+
if (now - cache.cachedAt > CACHE_TTL_MS)
|
|
54
|
+
return null;
|
|
55
|
+
return cache.spaceId;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeCachedDefaultSpace(key, spaceId) {
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
64
|
+
const cache = { key, spaceId, cachedAt: Date.now() };
|
|
65
|
+
writeFileSync(CACHE_PATH, `${JSON.stringify(cache, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Cache is best-effort; never fail the command over it.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let defaultSpacePromise = null;
|
|
72
|
+
export function clearDefaultSpaceCache() {
|
|
73
|
+
defaultSpacePromise = null;
|
|
74
|
+
try {
|
|
75
|
+
rmSync(CACHE_PATH, { force: true });
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Best-effort, same as writes.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Explicit target from `-s/--space` (any ancestor) or `COHUB_SPACE_ID`, else null. */
|
|
82
|
+
export function explicitSpace(program) {
|
|
3
83
|
let current = program;
|
|
4
84
|
while (current) {
|
|
5
85
|
const opts = current.opts();
|
|
@@ -7,8 +87,39 @@ export function resolveSpace(program) {
|
|
|
7
87
|
return opts.space.trim();
|
|
8
88
|
current = current.parent ?? null;
|
|
9
89
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
90
|
+
return process.env.COHUB_SPACE_ID?.trim() || null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the user's home space when no target is given. Cached locally per
|
|
94
|
+
* identity so repeated invocations skip the network entirely, and memoized
|
|
95
|
+
* in-process so preAction hooks and actions share a single lookup.
|
|
96
|
+
* Network and auth failures propagate so callers can report them faithfully.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveDefaultSpace() {
|
|
99
|
+
defaultSpacePromise ??= (async () => {
|
|
100
|
+
const key = identityKey();
|
|
101
|
+
if (key) {
|
|
102
|
+
const cached = readDefaultSpaceCache(CACHE_PATH, key);
|
|
103
|
+
if (cached)
|
|
104
|
+
return cached;
|
|
105
|
+
}
|
|
106
|
+
const space = (await createClient().spaces.getDefault()).space ?? null;
|
|
107
|
+
// Recent-space fallback from getDefault() is not stable enough to cache.
|
|
108
|
+
if (space?.id && space.slug === "home" && key)
|
|
109
|
+
writeCachedDefaultSpace(key, space.id);
|
|
110
|
+
return space?.id ?? null;
|
|
111
|
+
})();
|
|
112
|
+
return defaultSpacePromise;
|
|
113
|
+
}
|
|
114
|
+
/** Shared exit for commands that need a space but resolved none. */
|
|
115
|
+
export function missingSpaceError() {
|
|
116
|
+
return error("No target space", "Add -s, --space <id> or set COHUB_SPACE_ID. Run `cohub auth login` to use your home space.");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Target space for a command: explicit `-s`/`COHUB_SPACE_ID` first, then the
|
|
120
|
+
* user's home space. Exits with guidance when neither is available; request
|
|
121
|
+
* failures go through the shared HTTP error handler.
|
|
122
|
+
*/
|
|
123
|
+
export async function resolveSpace(program) {
|
|
124
|
+
return explicitSpace(program) ?? (await resolveDefaultSpace().catch(handleHttp)) ?? missingSpaceError();
|
|
14
125
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.8.1",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"commander": "^15.0.0",
|
|
20
20
|
"pixi.js": "^8.20.1",
|
|
21
21
|
"sharp": "^0.35.4",
|
|
22
|
-
"@neta-art/cohub": "8.10.
|
|
22
|
+
"@neta-art/cohub": "8.10.2"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
25
|
"access": "public"
|