@assethub/cli 0.1.30 → 0.1.32
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 +282 -8
- package/dist/appEnv.d.ts +20 -0
- package/dist/appEnv.d.ts.map +1 -0
- package/dist/appEnv.js +62 -0
- package/dist/canvasGraphDownload.d.ts +11 -0
- package/dist/canvasGraphDownload.d.ts.map +1 -0
- package/dist/canvasGraphDownload.js +108 -0
- package/dist/execution.d.ts +5 -2
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +11 -12
- package/dist/hooks/command.d.ts +14 -0
- package/dist/hooks/command.d.ts.map +1 -0
- package/dist/hooks/command.js +181 -0
- package/dist/hooks/graph.d.ts +40 -0
- package/dist/hooks/graph.d.ts.map +1 -0
- package/dist/hooks/graph.js +269 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/install.d.ts +58 -0
- package/dist/hooks/install.d.ts.map +1 -0
- package/dist/hooks/install.js +286 -0
- package/dist/hooks/paths.d.ts +31 -0
- package/dist/hooks/paths.d.ts.map +1 -0
- package/dist/hooks/paths.js +101 -0
- package/dist/hooks/redact.d.ts +5 -0
- package/dist/hooks/redact.d.ts.map +1 -0
- package/dist/hooks/redact.js +67 -0
- package/dist/hooks/save.d.ts +62 -0
- package/dist/hooks/save.d.ts.map +1 -0
- package/dist/hooks/save.js +331 -0
- package/dist/hooks/transcript.d.ts +23 -0
- package/dist/hooks/transcript.d.ts.map +1 -0
- package/dist/hooks/transcript.js +78 -0
- package/dist/hooks/trim.d.ts +25 -0
- package/dist/hooks/trim.d.ts.map +1 -0
- package/dist/hooks/trim.js +220 -0
- package/dist/hooks/types.d.ts +56 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +0 -0
- package/dist/hooks/upload.d.ts +52 -0
- package/dist/hooks/upload.d.ts.map +1 -0
- package/dist/hooks/upload.js +252 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1569 -165
- package/dist/nodeMeshBatch.d.ts.map +1 -1
- package/dist/nodeMeshBatch.js +5 -4
- package/dist/productionBatch.d.ts +77 -0
- package/dist/productionBatch.d.ts.map +1 -0
- package/dist/productionBatch.js +204 -0
- package/dist/runProgress.d.ts +4 -0
- package/dist/runProgress.d.ts.map +1 -0
- package/dist/runProgress.js +16 -0
- package/dist/runsUpload/controlEndpoints.d.ts +1 -1
- package/dist/runsUpload/controlEndpoints.d.ts.map +1 -1
- package/dist/runsUpload/controlEndpoints.js +4 -2
- package/dist/runsUpload/uploadRun.d.ts +7 -1
- package/dist/runsUpload/uploadRun.d.ts.map +1 -1
- package/dist/runsUpload/uploadRun.js +7 -4
- package/dist/setupCommand.d.ts +130 -0
- package/dist/setupCommand.d.ts.map +1 -0
- package/dist/setupCommand.js +569 -0
- package/dist/setupHooksBridge.d.ts +12 -0
- package/dist/setupHooksBridge.d.ts.map +1 -0
- package/dist/setupHooksBridge.js +4 -0
- package/dist/update.d.ts +74 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/update.js +205 -0
- package/package.json +2 -2
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access, copyFile, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
6
|
+
import { API_KEY_ENV, launchAgentPath, launchAgentPlist, loadKeyIntoLaunchd, readLaunchdKey } from './appEnv.js';
|
|
7
|
+
import { hookScopeError, sessionSaveDisclosure } from './hooks/install.js';
|
|
8
|
+
import { mcpConfig } from './setup.js';
|
|
9
|
+
export const redactKey = (key) => `ah_…${key.slice(-4)}`;
|
|
10
|
+
const scrub = (text, key) => key ? text.split(key).join(redactKey(key)) : text;
|
|
11
|
+
const TOML_HEADER = /^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/;
|
|
12
|
+
// `[mcp_servers."assethub"]` and `[mcp_servers.'assethub']` name the same table
|
|
13
|
+
// as `[mcp_servers.assethub]`, so quotes around a key segment are dropped.
|
|
14
|
+
// Whitespace is dropped only outside quotes: `"asset hub"` is another server.
|
|
15
|
+
const tableName = (line) => {
|
|
16
|
+
const match = TOML_HEADER.exec(line);
|
|
17
|
+
if (!match)
|
|
18
|
+
return undefined;
|
|
19
|
+
let name = '';
|
|
20
|
+
let quote;
|
|
21
|
+
for (const char of match[1]) {
|
|
22
|
+
if (quote) {
|
|
23
|
+
if (char === quote)
|
|
24
|
+
quote = undefined;
|
|
25
|
+
else
|
|
26
|
+
name += char;
|
|
27
|
+
}
|
|
28
|
+
else if (char === '"' || char === "'")
|
|
29
|
+
quote = char;
|
|
30
|
+
else if (!/\s/.test(char))
|
|
31
|
+
name += char;
|
|
32
|
+
}
|
|
33
|
+
return name;
|
|
34
|
+
};
|
|
35
|
+
const isAssetHubTable = (name) => name === 'mcp_servers.assethub' || name.startsWith('mcp_servers.assethub.');
|
|
36
|
+
// `assethub.url = …` or `assethub = { … }` under [mcp_servers], or a top-level
|
|
37
|
+
// `mcp_servers.assethub.… = …`: the same server, written as keys rather than a
|
|
38
|
+
// table. Appending a table next to it would be a duplicate key.
|
|
39
|
+
const ASSETHUB_KEY_IN_SERVERS = /^\s*["']?assethub["']?\s*[.=]/;
|
|
40
|
+
const ASSETHUB_KEY_AT_ROOT = /^\s*mcp_servers\s*\.\s*["']?assethub["']?\s*[.=]/;
|
|
41
|
+
// Replaces only the assethub table (and its sub-tables); every other line,
|
|
42
|
+
// including comments, blank lines and unrelated servers, is preserved.
|
|
43
|
+
export const mergeCodexSection = (existing, section) => {
|
|
44
|
+
const kept = [];
|
|
45
|
+
let current = '';
|
|
46
|
+
let inside = false;
|
|
47
|
+
let replacedAt = -1;
|
|
48
|
+
for (const line of existing.split('\n')) {
|
|
49
|
+
const name = tableName(line);
|
|
50
|
+
if (name !== undefined) {
|
|
51
|
+
current = name;
|
|
52
|
+
inside = isAssetHubTable(name);
|
|
53
|
+
if (inside && replacedAt < 0)
|
|
54
|
+
replacedAt = kept.length;
|
|
55
|
+
}
|
|
56
|
+
else if ((current === 'mcp_servers' && ASSETHUB_KEY_IN_SERVERS.test(line)) ||
|
|
57
|
+
(current === '' && ASSETHUB_KEY_AT_ROOT.test(line))) {
|
|
58
|
+
throw new Error('the assethub MCP server is already defined with dotted or inline keys; ' +
|
|
59
|
+
'replace it with an [mcp_servers.assethub] table (or remove it) and run setup again');
|
|
60
|
+
}
|
|
61
|
+
if (!inside)
|
|
62
|
+
kept.push(line);
|
|
63
|
+
}
|
|
64
|
+
const block = section.replace(/\n+$/, '').split('\n');
|
|
65
|
+
if (replacedAt >= 0) {
|
|
66
|
+
// Tidy blank lines only where the table is spliced in.
|
|
67
|
+
const before = kept.slice(0, replacedAt);
|
|
68
|
+
const after = kept.slice(replacedAt);
|
|
69
|
+
while (before.length > 0 && before[before.length - 1].trim() === '')
|
|
70
|
+
before.pop();
|
|
71
|
+
while (after.length > 0 && after[0].trim() === '')
|
|
72
|
+
after.shift();
|
|
73
|
+
const lines = [...before, ...(before.length > 0 ? [''] : []), ...block, ...(after.length > 0 ? ['', ...after] : [])];
|
|
74
|
+
return lines.join('\n').replace(/\n*$/, '\n');
|
|
75
|
+
}
|
|
76
|
+
const base = existing.replace(/\n+$/, '');
|
|
77
|
+
return `${base}${base ? '\n\n' : ''}${block.join('\n')}\n`;
|
|
78
|
+
};
|
|
79
|
+
export const extractCodexSection = (content) => {
|
|
80
|
+
const out = [];
|
|
81
|
+
let inside = false;
|
|
82
|
+
for (const line of content.split('\n')) {
|
|
83
|
+
const name = tableName(line);
|
|
84
|
+
if (name !== undefined)
|
|
85
|
+
inside = isAssetHubTable(name);
|
|
86
|
+
if (inside)
|
|
87
|
+
out.push(line);
|
|
88
|
+
}
|
|
89
|
+
return out.join('\n').replace(/\n+$/, '');
|
|
90
|
+
};
|
|
91
|
+
const sectionDiff = (before, after) => [
|
|
92
|
+
...(before ? before.split('\n') : []).map(line => `- ${line}`),
|
|
93
|
+
...after.split('\n').map(line => `+ ${line}`),
|
|
94
|
+
];
|
|
95
|
+
const timestamp = (date) => date.toISOString().replace(/[-:.]/g, '');
|
|
96
|
+
// MCP first: the Claude Code entry works without the CLI installed. The key is
|
|
97
|
+
// referenced as ${ASSETHUB_API_KEY}, which Claude Code expands from its
|
|
98
|
+
// environment on every connection (verified with Claude Code 2.1.81), so it is
|
|
99
|
+
// never written to ~/.claude.json or passed on a command line. Same contract as
|
|
100
|
+
// the Codex section's bearer_token_env_var.
|
|
101
|
+
export { API_KEY_ENV };
|
|
102
|
+
export const claudeServerConfig = (baseUrl, workspaceId) => ({
|
|
103
|
+
type: 'http',
|
|
104
|
+
url: `${baseUrl.replace(/\/+$/, '')}/api/mcp`,
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer \${${API_KEY_ENV}}`,
|
|
107
|
+
...(workspaceId ? { 'X-AssetHub-Workspace': workspaceId } : {}),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
export const claudeAddArgs = (baseUrl, workspaceId) => [
|
|
111
|
+
'mcp', 'add-json', 'assethub', JSON.stringify(claudeServerConfig(baseUrl, workspaceId)), '--scope', 'user',
|
|
112
|
+
];
|
|
113
|
+
const shellQuoteSingle = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
114
|
+
export const runSetup = async (options, deps) => {
|
|
115
|
+
const steps = [];
|
|
116
|
+
const say = (line) => deps.log(line);
|
|
117
|
+
const wantClaude = options.client !== 'codex';
|
|
118
|
+
const wantCodex = options.client !== 'claude';
|
|
119
|
+
const hookClients = [...(wantClaude ? ['claude'] : []), ...(wantCodex ? ['codex'] : [])];
|
|
120
|
+
const hookNames = hookClients.map(client => (client === 'claude' ? 'Claude Code' : 'Codex')).join(' and ');
|
|
121
|
+
let hookRefusal;
|
|
122
|
+
for (const client of hookClients) {
|
|
123
|
+
const refused = await hookScopeError(deps.home, { projectDir: deps.cwd }, client, deps.codexHome);
|
|
124
|
+
if (refused) {
|
|
125
|
+
hookRefusal = `${client}: ${refused}`;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 0. --save-sessions asks for something the home folder cannot have, so stop
|
|
130
|
+
// before login, MCP or app-env changes rather than half-applying setup.
|
|
131
|
+
if (hookRefusal && options.saveSessions && !options.noHook) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
dryRun: options.dryRun,
|
|
135
|
+
profile: '',
|
|
136
|
+
baseUrl: '',
|
|
137
|
+
steps: [{ name: 'hook', status: 'fail', detail: hookRefusal }],
|
|
138
|
+
nextStep: 'Run `assethub setup --save-sessions` inside the project folder whose sessions should be saved, or run it without --save-sessions.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// 1. Login (reuses the existing `auth login` path through deps.login)
|
|
142
|
+
const explicitKey = options.apiKeySource === 'flag' ? options.apiKey : undefined;
|
|
143
|
+
const hadKey = explicitKey ? false : await deps.hasWorkingKey();
|
|
144
|
+
const givenKey = explicitKey ?? (options.apiKeySource === 'env' ? options.apiKey : undefined);
|
|
145
|
+
const givenFrom = explicitKey ? '--api-key' : 'ASSETHUB_API_KEY';
|
|
146
|
+
if (hadKey) {
|
|
147
|
+
steps.push({ name: 'login', status: 'ok', detail: 'reusing the saved API key' });
|
|
148
|
+
}
|
|
149
|
+
else if (options.dryRun) {
|
|
150
|
+
steps.push({
|
|
151
|
+
name: 'login',
|
|
152
|
+
status: 'skip',
|
|
153
|
+
detail: givenKey
|
|
154
|
+
? `would verify and save the API key from ${givenFrom}`
|
|
155
|
+
: options.apiKeyStdin
|
|
156
|
+
? 'would read the API key from stdin'
|
|
157
|
+
: 'would prompt for an API key',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
let apiKey = givenKey;
|
|
162
|
+
if (!apiKey && !options.apiKeyStdin) {
|
|
163
|
+
if (!deps.interactive)
|
|
164
|
+
throw new Error('No saved API key. Re-run with --api-key-stdin, set ASSETHUB_API_KEY, or run interactively to be prompted.');
|
|
165
|
+
apiKey = await deps.promptApiKey();
|
|
166
|
+
}
|
|
167
|
+
await deps.login({ apiKey });
|
|
168
|
+
steps.push({
|
|
169
|
+
name: 'login',
|
|
170
|
+
status: 'ok',
|
|
171
|
+
detail: givenKey ? `API key from ${givenFrom} verified and saved` : 'API key verified and saved',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
let auth;
|
|
175
|
+
if (!options.dryRun || hadKey)
|
|
176
|
+
auth = await deps.resolveAuth();
|
|
177
|
+
const key = auth?.apiKey ?? '';
|
|
178
|
+
const baseUrl = (auth?.baseUrl ?? '').replace(/\/+$/, '');
|
|
179
|
+
// 2. Workspace
|
|
180
|
+
let workspaceId = auth?.workspaceId;
|
|
181
|
+
if (options.workspace) {
|
|
182
|
+
workspaceId = options.workspace;
|
|
183
|
+
if (options.dryRun) {
|
|
184
|
+
steps.push({ name: 'workspace', status: 'skip', detail: `would select workspace ${workspaceId}` });
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
await deps.useWorkspace(options.workspace);
|
|
188
|
+
auth = await deps.resolveAuth();
|
|
189
|
+
steps.push({ name: 'workspace', status: 'ok', detail: `selected ${workspaceId}` });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (workspaceId) {
|
|
193
|
+
steps.push({ name: 'workspace', status: 'ok', detail: `using ${workspaceId}` });
|
|
194
|
+
}
|
|
195
|
+
else if (auth && (workspaceId = await deps.discoverWorkspace(auth))) {
|
|
196
|
+
steps.push({ name: 'workspace', status: 'ok', detail: `using ${workspaceId} (from the API key)` });
|
|
197
|
+
}
|
|
198
|
+
else if (options.dryRun) {
|
|
199
|
+
steps.push({ name: 'workspace', status: 'skip', detail: 'would list workspaces and ask you to pick one' });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
if (!deps.interactive)
|
|
203
|
+
throw new Error('No workspace selected. Re-run with --workspace <id> (see: assethub workspace list).');
|
|
204
|
+
const items = await deps.listWorkspaces();
|
|
205
|
+
if (items.length === 0)
|
|
206
|
+
throw new Error('No workspaces are available to this account.');
|
|
207
|
+
workspaceId = items.length === 1 ? items[0].id : await deps.pickWorkspace(items);
|
|
208
|
+
await deps.useWorkspace(workspaceId);
|
|
209
|
+
auth = await deps.resolveAuth();
|
|
210
|
+
steps.push({ name: 'workspace', status: 'ok', detail: `selected ${workspaceId}` });
|
|
211
|
+
}
|
|
212
|
+
const workspaceForConfig = workspaceId ?? '<workspace-id>';
|
|
213
|
+
const shownKey = key ? redactKey(key) : 'ah_…<key>';
|
|
214
|
+
if (!options.dryRun &&
|
|
215
|
+
deps.interactive &&
|
|
216
|
+
!options.yes &&
|
|
217
|
+
!(await deps.confirm('Write MCP configuration for the selected client(s)?')))
|
|
218
|
+
throw new Error('Cancelled before changing any client configuration.');
|
|
219
|
+
// 3. Claude Code
|
|
220
|
+
if (wantClaude) {
|
|
221
|
+
const claude = await deps.findBinary('claude');
|
|
222
|
+
const args = claudeAddArgs(baseUrl || '<base-url>', workspaceForConfig);
|
|
223
|
+
const shown = `claude ${args.slice(0, 3).join(' ')} ${shellQuoteSingle(args[3])} ${args.slice(4).join(' ')}`;
|
|
224
|
+
if (!claude) {
|
|
225
|
+
say(`claude not found on PATH. Run this later:\n ${shown}`);
|
|
226
|
+
// Claude was the only client asked for, so nothing got configured: say so
|
|
227
|
+
// in the exit code rather than reporting success.
|
|
228
|
+
steps.push({
|
|
229
|
+
name: 'claude-mcp',
|
|
230
|
+
status: wantCodex ? 'skip' : 'fail',
|
|
231
|
+
detail: 'claude CLI not found; run the printed command later',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if (options.dryRun) {
|
|
235
|
+
say(`[dry-run] ${shown}`);
|
|
236
|
+
say('[dry-run] if an assethub entry already exists: claude mcp remove assethub --scope user, then add again');
|
|
237
|
+
steps.push({ name: 'claude-mcp', status: 'skip', detail: 'would register the assethub MCP server (dry run)' });
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Add first, and replace only an entry that already exists, so a failing
|
|
241
|
+
// add never costs the user a working one.
|
|
242
|
+
const addArgs = claudeAddArgs(baseUrl, workspaceForConfig);
|
|
243
|
+
let added = await deps.run(claude, addArgs);
|
|
244
|
+
let replaced = false;
|
|
245
|
+
if (added.code !== 0 && /already exists/i.test(added.output)) {
|
|
246
|
+
await deps.run(claude, ['mcp', 'remove', 'assethub', '--scope', 'user']);
|
|
247
|
+
replaced = true;
|
|
248
|
+
added = await deps.run(claude, addArgs);
|
|
249
|
+
}
|
|
250
|
+
const failure = scrub(added.output, key).trim().slice(0, 300);
|
|
251
|
+
steps.push(added.code === 0
|
|
252
|
+
? {
|
|
253
|
+
name: 'claude-mcp',
|
|
254
|
+
status: 'ok',
|
|
255
|
+
detail: `registered assethub in Claude Code (user scope; it reads the key from \$${API_KEY_ENV}, the key is not stored in Claude Code)`,
|
|
256
|
+
}
|
|
257
|
+
: {
|
|
258
|
+
name: 'claude-mcp',
|
|
259
|
+
status: 'fail',
|
|
260
|
+
detail: replaced
|
|
261
|
+
? `claude mcp add failed after the old assethub entry was removed; run \`${shown}\` to restore it: ${failure}`
|
|
262
|
+
: `claude mcp add failed: ${failure}`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// 4. Codex
|
|
267
|
+
let exportLine;
|
|
268
|
+
if (wantCodex) {
|
|
269
|
+
const path = join(deps.codexHome, 'config.toml');
|
|
270
|
+
const section = mcpConfig('codex', baseUrl || 'https://app.assethub.io', false, workspaceId);
|
|
271
|
+
const existing = await deps.readFileIfExists(path);
|
|
272
|
+
let merged;
|
|
273
|
+
let mergeError;
|
|
274
|
+
try {
|
|
275
|
+
merged = mergeCodexSection(existing ?? '', section);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
mergeError = error instanceof Error ? error.message : String(error);
|
|
279
|
+
}
|
|
280
|
+
if (merged === undefined) {
|
|
281
|
+
steps.push({ name: 'codex-mcp', status: 'fail', detail: `${path} left unchanged: ${mergeError}` });
|
|
282
|
+
}
|
|
283
|
+
else if (options.dryRun) {
|
|
284
|
+
say(`[dry-run] would ${existing === undefined ? 'create' : 'update'} ${path}`);
|
|
285
|
+
if (existing !== undefined)
|
|
286
|
+
say(`[dry-run] would back up to ${path}.bak-<timestamp>`);
|
|
287
|
+
for (const line of sectionDiff(extractCodexSection(existing ?? ''), extractCodexSection(merged)))
|
|
288
|
+
say(` ${line}`);
|
|
289
|
+
steps.push({ name: 'codex-mcp', status: 'skip', detail: `would write [mcp_servers.assethub] to ${path} (dry run)` });
|
|
290
|
+
}
|
|
291
|
+
else if (merged === existing) {
|
|
292
|
+
steps.push({ name: 'codex-mcp', status: 'ok', detail: `${path} already up to date` });
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
let backup = '';
|
|
296
|
+
if (existing !== undefined) {
|
|
297
|
+
// Never overwrite an earlier backup made in the same instant.
|
|
298
|
+
const base = `${path}.bak-${timestamp(deps.now())}`;
|
|
299
|
+
backup = base;
|
|
300
|
+
for (let n = 1; (await deps.readFileIfExists(backup)) !== undefined; n++)
|
|
301
|
+
backup = `${base}-${n}`;
|
|
302
|
+
await deps.backupFile(path, backup);
|
|
303
|
+
}
|
|
304
|
+
await deps.writeFileEnsuringDir(path, merged);
|
|
305
|
+
steps.push({ name: 'codex-mcp', status: 'ok', detail: `wrote [mcp_servers.assethub] to ${path}${backup ? ` (backup: ${backup})` : ''}` });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Both clients read the key from the environment; the MCP server needs nothing else.
|
|
309
|
+
say(`${wantClaude && wantCodex ? 'Claude Code and Codex read' : wantClaude ? 'Claude Code reads' : 'Codex reads'} the key from the ${API_KEY_ENV} environment variable: export it in your shell (run \`assethub setup --print-env\` to print the line). Setup never writes it to a shell profile.`);
|
|
310
|
+
if (options.printEnv)
|
|
311
|
+
exportLine = `export ${API_KEY_ENV}=${options.dryRun || !key ? shownKey : key}`;
|
|
312
|
+
// 5. Make the key visible to apps started outside a shell (macOS).
|
|
313
|
+
const appEnv = await ensureAppEnv(options, deps, key);
|
|
314
|
+
steps.push(appEnv);
|
|
315
|
+
// 6. Session saving: opt-in, and offered only to accounts that may upload
|
|
316
|
+
// sessions (internal for now). Anyone else gets no question and no step.
|
|
317
|
+
const sessionsAvailable = auth != null && (await deps.canSaveSessions(auth));
|
|
318
|
+
if (!sessionsAvailable) {
|
|
319
|
+
if (options.saveSessions)
|
|
320
|
+
steps.push({ name: 'hook', status: 'skip', detail: 'session saving is not available for this account' });
|
|
321
|
+
}
|
|
322
|
+
else if (options.noHook) {
|
|
323
|
+
steps.push({ name: 'hook', status: 'skip', detail: 'skipped (--no-hook)' });
|
|
324
|
+
}
|
|
325
|
+
else if (hookRefusal) {
|
|
326
|
+
// Not offered: the dry run and the question would promise an install the guard refuses.
|
|
327
|
+
steps.push({ name: 'hook', status: 'skip', detail: hookRefusal });
|
|
328
|
+
}
|
|
329
|
+
else if (options.dryRun) {
|
|
330
|
+
steps.push({
|
|
331
|
+
name: 'hook',
|
|
332
|
+
status: 'skip',
|
|
333
|
+
detail: options.saveSessions
|
|
334
|
+
? `would install the ${hookNames} session-saving hooks for ${deps.cwd} only (dry run)`
|
|
335
|
+
: 'would leave session saving off (enable with --save-sessions)',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const hookScope = { projectDir: deps.cwd };
|
|
340
|
+
let consent = options.saveSessions;
|
|
341
|
+
const disclose = () => {
|
|
342
|
+
for (const client of hookClients)
|
|
343
|
+
say(sessionSaveDisclosure(hookScope, client));
|
|
344
|
+
};
|
|
345
|
+
if (!consent && deps.interactive && !options.yes) {
|
|
346
|
+
disclose();
|
|
347
|
+
consent = await deps.confirmOptIn(`Save and upload your ${hookNames} sessions?`);
|
|
348
|
+
}
|
|
349
|
+
if (!consent) {
|
|
350
|
+
steps.push({
|
|
351
|
+
name: 'hook',
|
|
352
|
+
status: 'skip',
|
|
353
|
+
detail: `session saving is off; enable it with \`assethub setup --save-sessions\`, or ${hookClients.map(client => `\`assethub hooks install --client ${client}\``).join(' and ')} inside the project folder`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
if (options.saveSessions)
|
|
358
|
+
disclose();
|
|
359
|
+
const pathNote = (await deps.findBinary('assethub'))
|
|
360
|
+
? ''
|
|
361
|
+
: ' Note: the hooks run `assethub`, which is not on PATH; install the CLI globally.';
|
|
362
|
+
for (const client of hookClients) {
|
|
363
|
+
try {
|
|
364
|
+
const result = await deps.installHooks({
|
|
365
|
+
client,
|
|
366
|
+
...hookScope,
|
|
367
|
+
...(client === 'codex' ? { codexHome: deps.codexHome } : {}),
|
|
368
|
+
});
|
|
369
|
+
steps.push({
|
|
370
|
+
name: 'hook',
|
|
371
|
+
status: result.installed ? 'ok' : 'fail',
|
|
372
|
+
detail: `${client}: ${result.detail}${result.installed ? pathNote : ''}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
steps.push({
|
|
377
|
+
name: 'hook',
|
|
378
|
+
status: 'fail',
|
|
379
|
+
detail: `${client}: ${scrub(error instanceof Error ? error.message : String(error), key)}`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// 7. Doctor
|
|
386
|
+
if (options.dryRun) {
|
|
387
|
+
steps.push({ name: 'doctor', status: 'skip', detail: 'skipped (dry run)' });
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
try {
|
|
391
|
+
const report = await deps.diagnose();
|
|
392
|
+
const failing = report.checks.filter(check => check.status !== 'pass');
|
|
393
|
+
steps.push({
|
|
394
|
+
name: 'doctor',
|
|
395
|
+
status: report.ok ? 'ok' : 'fail',
|
|
396
|
+
detail: report.ok
|
|
397
|
+
? 'API and MCP checks passed'
|
|
398
|
+
: failing.map(check => `${check.name}: ${check.message ?? 'failed'}`).join('; '),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
steps.push({ name: 'doctor', status: 'fail', detail: scrub(error instanceof Error ? error.message : String(error), key) });
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const ok = steps.every(step => step.status !== 'fail');
|
|
406
|
+
const nextStep = options.dryRun
|
|
407
|
+
? 'Re-run without --dry-run to apply these changes.'
|
|
408
|
+
: !ok
|
|
409
|
+
? 'Fix the failed steps above, then re-run `assethub setup` (it is safe to repeat).'
|
|
410
|
+
: appEnv.status === 'ok'
|
|
411
|
+
? `Quit and reopen ${wantClaude && wantCodex ? 'Claude (Cmd+Q) and Codex' : wantClaude ? 'Claude (Cmd+Q)' : 'Codex'} so it reads ${API_KEY_ENV}, then ask it to use the AssetHub tools. Terminal tabs opened before setup still need \`export ${API_KEY_ENV}=…\`.`
|
|
412
|
+
: `Export ${API_KEY_ENV} in the shell that starts ${wantClaude && wantCodex ? 'Claude Code / Codex' : wantClaude ? 'Claude Code' : 'Codex'}, restart it, then ask it to use the AssetHub tools.`;
|
|
413
|
+
return {
|
|
414
|
+
ok,
|
|
415
|
+
dryRun: options.dryRun,
|
|
416
|
+
profile: auth?.profile ?? '',
|
|
417
|
+
baseUrl,
|
|
418
|
+
workspaceId,
|
|
419
|
+
steps,
|
|
420
|
+
nextStep,
|
|
421
|
+
...(exportLine ? { exportLine } : {}),
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
const APP_ENV_MISSING = `Claude and Codex apps cannot see ${API_KEY_ENV} yet, so their MCP calls get HTTP 401`;
|
|
425
|
+
const ensureAppEnv = async (options, deps, key) => {
|
|
426
|
+
const name = 'app-env';
|
|
427
|
+
if (options.noAppEnv)
|
|
428
|
+
return { name, status: 'skip', detail: 'skipped (--no-app-env)' };
|
|
429
|
+
if (deps.platform !== 'darwin')
|
|
430
|
+
return {
|
|
431
|
+
name,
|
|
432
|
+
status: 'skip',
|
|
433
|
+
detail: `set ${API_KEY_ENV} in the environment your apps start with (\`assethub setup --print-env\` prints the line)`,
|
|
434
|
+
};
|
|
435
|
+
const plistPath = launchAgentPath(deps.home);
|
|
436
|
+
if (options.dryRun) {
|
|
437
|
+
deps.log(`[dry-run] would run: launchctl setenv ${API_KEY_ENV} ${key ? redactKey(key) : 'ah_…<key>'}`);
|
|
438
|
+
deps.log(`[dry-run] would install ${plistPath} (runs \`assethub env load\` at login; holds no key)`);
|
|
439
|
+
return { name, status: 'skip', detail: 'would make the key visible to apps (dry run)' };
|
|
440
|
+
}
|
|
441
|
+
if (!key)
|
|
442
|
+
return { name, status: 'skip', detail: 'no API key to publish' };
|
|
443
|
+
const plist = launchAgentPlist(deps.launchAgentProgram);
|
|
444
|
+
if ((await readLaunchdKey(deps.run)) === key) {
|
|
445
|
+
// A login agent from an older CLI may pin a node that is later removed;
|
|
446
|
+
// rewrite ours in place. With no agent, the key came from elsewhere: leave it.
|
|
447
|
+
const installed = await deps.readFileIfExists(plistPath);
|
|
448
|
+
if (installed === undefined || installed === plist)
|
|
449
|
+
return { name, status: 'ok', detail: `${API_KEY_ENV} is already visible to apps` };
|
|
450
|
+
await deps.writeFileEnsuringDir(plistPath, plist);
|
|
451
|
+
return { name, status: 'ok', detail: `${API_KEY_ENV} is already visible to apps; refreshed ${plistPath}` };
|
|
452
|
+
}
|
|
453
|
+
const consent = options.yes ||
|
|
454
|
+
(deps.interactive &&
|
|
455
|
+
(await deps.confirm(`Make ${API_KEY_ENV} visible to Claude and Codex apps (launchctl setenv, re-applied at login)?`)));
|
|
456
|
+
if (!consent)
|
|
457
|
+
return { name, status: 'skip', detail: `${APP_ENV_MISSING}; re-run \`assethub setup --yes\` or export it before starting them` };
|
|
458
|
+
await deps.writeFileEnsuringDir(plistPath, plist);
|
|
459
|
+
const loaded = await loadKeyIntoLaunchd(key, deps.run);
|
|
460
|
+
if (!loaded.ok)
|
|
461
|
+
return { name, status: 'fail', detail: `${APP_ENV_MISSING}: ${loaded.detail}` };
|
|
462
|
+
if ((await readLaunchdKey(deps.run)) !== key)
|
|
463
|
+
return { name, status: 'fail', detail: `${APP_ENV_MISSING}: launchctl did not keep the value` };
|
|
464
|
+
return { name, status: 'ok', detail: `${API_KEY_ENV} is visible to apps started from now on; ${plistPath} re-applies it at login` };
|
|
465
|
+
};
|
|
466
|
+
export const formatSetupSummary = (result) => {
|
|
467
|
+
const mark = { ok: '✓', fail: '✗', skip: '-' };
|
|
468
|
+
const lines = result.steps.map(step => `${mark[step.status]} ${step.name}: ${step.detail}`);
|
|
469
|
+
return `${lines.join('\n')}\n\nNext: ${result.nextStep}\n`;
|
|
470
|
+
};
|
|
471
|
+
// ---- default (real) implementations of the environment-facing dependencies ----
|
|
472
|
+
export const findOnPath = async (name, pathValue = process.env.PATH ?? '') => {
|
|
473
|
+
for (const dir of pathValue.split(delimiter).filter(Boolean)) {
|
|
474
|
+
const candidate = join(dir, name);
|
|
475
|
+
try {
|
|
476
|
+
// Only an executable file: a directory or a plain file of that name is not a usable command.
|
|
477
|
+
await access(candidate, constants.X_OK);
|
|
478
|
+
if ((await stat(candidate)).isFile())
|
|
479
|
+
return candidate;
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
/* try the next directory */
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return undefined;
|
|
486
|
+
};
|
|
487
|
+
export const runProcess = (file, args) => new Promise(done => {
|
|
488
|
+
execFile(file, args, { timeout: 60_000 }, (error, stdout, stderr) => {
|
|
489
|
+
const code = error ? (typeof error.code === 'number' ? error.code : 1) : 0;
|
|
490
|
+
done({ code, output: `${stdout}${stderr}` });
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
export const defaultFileDeps = () => ({
|
|
494
|
+
platform: process.platform,
|
|
495
|
+
home: homedir(),
|
|
496
|
+
cwd: process.cwd(),
|
|
497
|
+
codexHome: process.env.CODEX_HOME?.trim() || join(homedir(), '.codex'),
|
|
498
|
+
now: () => new Date(),
|
|
499
|
+
findBinary: (name) => findOnPath(name),
|
|
500
|
+
run: runProcess,
|
|
501
|
+
readFileIfExists: async (path) => {
|
|
502
|
+
try {
|
|
503
|
+
return await readFile(path, 'utf8');
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
if (error.code === 'ENOENT')
|
|
507
|
+
return undefined;
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
writeFileEnsuringDir: async (path, content) => {
|
|
512
|
+
await mkdir(dirname(path), { recursive: true });
|
|
513
|
+
await writeFile(path, content);
|
|
514
|
+
},
|
|
515
|
+
backupFile: (from, to) => copyFile(from, to),
|
|
516
|
+
});
|
|
517
|
+
// ---- interactive prompts (TTY only; prompts go to stderr so stdout stays clean) ----
|
|
518
|
+
export const promptLine = async (question) => {
|
|
519
|
+
const { createInterface } = await import('node:readline/promises');
|
|
520
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
521
|
+
try {
|
|
522
|
+
return (await rl.question(question)).trim();
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
rl.close();
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
// Reads a secret without echoing it.
|
|
529
|
+
export const promptHidden = (question) => new Promise((resolve, reject) => {
|
|
530
|
+
const input = process.stdin;
|
|
531
|
+
if (!input.isTTY)
|
|
532
|
+
return reject(new Error('A terminal is required to enter the API key interactively.'));
|
|
533
|
+
process.stderr.write(question);
|
|
534
|
+
let value = '';
|
|
535
|
+
input.setRawMode(true);
|
|
536
|
+
input.resume();
|
|
537
|
+
input.setEncoding('utf8');
|
|
538
|
+
const finish = (done) => {
|
|
539
|
+
input.setRawMode(false);
|
|
540
|
+
input.pause();
|
|
541
|
+
input.removeListener('data', onData);
|
|
542
|
+
process.stderr.write('\n');
|
|
543
|
+
done();
|
|
544
|
+
};
|
|
545
|
+
const onData = (chunk) => {
|
|
546
|
+
for (const char of chunk) {
|
|
547
|
+
if (char === '\r' || char === '\n')
|
|
548
|
+
return finish(() => resolve(value.trim()));
|
|
549
|
+
if (char === '\u0003')
|
|
550
|
+
return finish(() => reject(new Error('Cancelled.')));
|
|
551
|
+
if (char === '\u007f' || char === '\b')
|
|
552
|
+
value = value.slice(0, -1);
|
|
553
|
+
else
|
|
554
|
+
value += char;
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
input.on('data', onData);
|
|
558
|
+
});
|
|
559
|
+
export const pickFromList = async (items) => {
|
|
560
|
+
process.stderr.write('Workspaces:\n');
|
|
561
|
+
items.forEach((item, index) => process.stderr.write(` ${index + 1}) ${item.name ?? item.id} (${item.id})\n`));
|
|
562
|
+
const answer = Number(await promptLine(`Pick a workspace [1-${items.length}]: `));
|
|
563
|
+
const chosen = items[answer - 1];
|
|
564
|
+
if (!Number.isInteger(answer) || !chosen)
|
|
565
|
+
throw new Error('No valid workspace chosen.');
|
|
566
|
+
return chosen.id;
|
|
567
|
+
};
|
|
568
|
+
export const confirmYes = async (question) => !/^n/i.test(await promptLine(`${question} [Y/n] `));
|
|
569
|
+
export const confirmNo = async (question) => /^y/i.test(await promptLine(`${question} [y/N] `));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type InstallHooks = (opts: {
|
|
2
|
+
client: 'claude' | 'codex';
|
|
3
|
+
/** The project folder whose .claude/settings.local.json or .codex/hooks.json gets the hooks. */
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
/** Codex's user folder ($CODEX_HOME), where Codex records which projects are trusted. */
|
|
6
|
+
codexHome?: string;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
installed: boolean;
|
|
9
|
+
detail: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const defaultInstallHooks: InstallHooks;
|
|
12
|
+
//# sourceMappingURL=setupHooksBridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setupHooksBridge.d.ts","sourceRoot":"","sources":["../src/setupHooksBridge.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE;IAChC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAA;IAC1B,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,KAAK,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC,CAAA;AAEnD,eAAO,MAAM,mBAAmB,EAAE,YAGhC,CAAA"}
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export declare const PACKAGE_NAME = "@assethub/cli";
|
|
2
|
+
export type Installer = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'volta';
|
|
3
|
+
export type Install = {
|
|
4
|
+
kind: 'global';
|
|
5
|
+
installer: Installer;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'npx';
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'project';
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'source';
|
|
12
|
+
};
|
|
13
|
+
export type UpdateOptions = {
|
|
14
|
+
check?: boolean;
|
|
15
|
+
dryRun?: boolean;
|
|
16
|
+
yes?: boolean;
|
|
17
|
+
};
|
|
18
|
+
export type UpdateDeps = {
|
|
19
|
+
currentVersion: () => Promise<string>;
|
|
20
|
+
latestVersion: () => Promise<string>;
|
|
21
|
+
/** Real path of the running entry file, symlinks resolved. */
|
|
22
|
+
binPath: () => Promise<string>;
|
|
23
|
+
npmGlobalRoot: () => Promise<string | undefined>;
|
|
24
|
+
run: (command: string, args: string[]) => Promise<{
|
|
25
|
+
code: number;
|
|
26
|
+
}>;
|
|
27
|
+
/** Runs the freshly installed binary: its version and whether the saved login still works. */
|
|
28
|
+
verify: () => Promise<{
|
|
29
|
+
version: string;
|
|
30
|
+
auth: 'pass' | 'fail' | 'not_configured';
|
|
31
|
+
}>;
|
|
32
|
+
interactive: boolean;
|
|
33
|
+
confirm: (question: string) => Promise<boolean>;
|
|
34
|
+
say: (message: string) => void;
|
|
35
|
+
};
|
|
36
|
+
export type UpdateReport = {
|
|
37
|
+
status: 'up_to_date';
|
|
38
|
+
from: string;
|
|
39
|
+
latest: string;
|
|
40
|
+
} | {
|
|
41
|
+
status: 'update_available';
|
|
42
|
+
from: string;
|
|
43
|
+
latest: string;
|
|
44
|
+
} | {
|
|
45
|
+
status: 'dry_run';
|
|
46
|
+
from: string;
|
|
47
|
+
latest: string;
|
|
48
|
+
installer: Installer;
|
|
49
|
+
command: string;
|
|
50
|
+
} | {
|
|
51
|
+
status: 'updated';
|
|
52
|
+
from: string;
|
|
53
|
+
to: string;
|
|
54
|
+
installer: Installer;
|
|
55
|
+
auth: 'pass' | 'fail' | 'not_configured';
|
|
56
|
+
};
|
|
57
|
+
/** Semver precedence for plain `x.y.z[-pre]` versions (build metadata unused). */
|
|
58
|
+
export declare const compareVersions: (a: string, b: string) => number;
|
|
59
|
+
export declare const detectInstall: (binPath: string, npmGlobalRoot?: string) => Install;
|
|
60
|
+
export declare const installCommand: (installer: Installer, version: string) => [string, string[]];
|
|
61
|
+
export declare const runUpdate: (options: UpdateOptions, deps: UpdateDeps) => Promise<UpdateReport>;
|
|
62
|
+
/**
|
|
63
|
+
* Prefers the package manager, which applies the user's .npmrc registry,
|
|
64
|
+
* auth token and proxy exactly as the install will; falls back to a direct
|
|
65
|
+
* registry read only when that lookup cannot run (e.g. no npm on PATH).
|
|
66
|
+
*/
|
|
67
|
+
export declare const resolveLatestVersion: (viaPackageManager: () => Promise<string>, viaRegistry: () => Promise<string>) => Promise<string>;
|
|
68
|
+
/** Wires runUpdate to the real registry, package manager and freshly installed binary. */
|
|
69
|
+
/**
|
|
70
|
+
* `forwardFlags` reach the verifying `assethub doctor` on its command line;
|
|
71
|
+
* `forwardEnv` carries what must not appear there, such as an explicit API key.
|
|
72
|
+
*/
|
|
73
|
+
export declare const createNodeUpdateDeps: (forwardFlags: string[], forwardEnv?: Record<string, string>) => UpdateDeps;
|
|
74
|
+
//# sourceMappingURL=update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../src/update.ts"],"names":[],"mappings":"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"}
|