@oxygen-agent/cli 1.46.0 → 1.64.5
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 +13 -1
- package/dist/credentials.d.ts +25 -0
- package/dist/credentials.js +214 -45
- package/dist/index.js +1107 -181
- package/dist/local-custom-http-column.js +1 -1
- package/dist/skills.d.ts +41 -0
- package/dist/skills.js +325 -0
- package/dist/update.d.ts +34 -0
- package/dist/update.js +123 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/billing.js +2 -1
- package/node_modules/@oxygen/shared/dist/cell-format.d.ts +60 -0
- package/node_modules/@oxygen/shared/dist/cell-format.js +277 -0
- package/node_modules/@oxygen/shared/dist/column-types.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/column-types.js +3 -2
- package/node_modules/@oxygen/shared/dist/file-import.js +1 -1
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +2 -0
- package/node_modules/@oxygen/shared/dist/log.js +1 -1
- package/node_modules/@oxygen/shared/dist/provider-request-outcomes.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/provider-request-outcomes.js +5 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/workflows/dist/index.d.ts +145 -143
- package/node_modules/@oxygen/workflows/dist/index.js +66 -29
- package/package.json +1 -1
|
@@ -387,7 +387,7 @@ async function readLocalHttpResponseBody(response) {
|
|
|
387
387
|
if (!text)
|
|
388
388
|
return null;
|
|
389
389
|
const contentType = response.headers.get("content-type") ?? "";
|
|
390
|
-
const looksJson = contentType.toLowerCase().includes("json") || /^[\s\n\r]*[\[{]/.test(text);
|
|
390
|
+
const looksJson = contentType.toLowerCase().includes("json") || /^[\s\n\r]*[\[{]/.test(text); // skipcq: JS-0097
|
|
391
391
|
if (!looksJson)
|
|
392
392
|
return text;
|
|
393
393
|
try {
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
export type ExecFileSyncLike = typeof execFileSync;
|
|
3
|
+
export type SkillsInstallOptions = {
|
|
4
|
+
json?: boolean;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
agents?: string | string[];
|
|
7
|
+
skill?: string;
|
|
8
|
+
project?: boolean;
|
|
9
|
+
copy?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export type SkillsListOptions = {
|
|
12
|
+
json?: boolean;
|
|
13
|
+
apiUrl?: string;
|
|
14
|
+
};
|
|
15
|
+
export type SkillsDoctorOptions = {
|
|
16
|
+
json?: boolean;
|
|
17
|
+
apiUrl?: string;
|
|
18
|
+
};
|
|
19
|
+
export type AutomaticSkillsInstallResult = {
|
|
20
|
+
attempted: boolean;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
command: string;
|
|
23
|
+
reason?: string;
|
|
24
|
+
error?: {
|
|
25
|
+
code: string;
|
|
26
|
+
message: string;
|
|
27
|
+
details?: unknown;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export declare function listAgentSkills(options: SkillsListOptions): Promise<Record<string, unknown>>;
|
|
31
|
+
export declare function doctorAgentSkills(options: SkillsDoctorOptions): Promise<Record<string, unknown>>;
|
|
32
|
+
export declare function installAgentSkills(options: SkillsInstallOptions, runtime?: {
|
|
33
|
+
env?: NodeJS.ProcessEnv;
|
|
34
|
+
execFileSync?: ExecFileSyncLike;
|
|
35
|
+
}): Record<string, unknown>;
|
|
36
|
+
export declare function runAutomaticSkillsInstall(options?: {
|
|
37
|
+
apiUrl?: string;
|
|
38
|
+
env?: NodeJS.ProcessEnv;
|
|
39
|
+
execFileSync?: ExecFileSyncLike;
|
|
40
|
+
}): AutomaticSkillsInstallResult;
|
|
41
|
+
export declare function skippedAutomaticSkillsInstall(reason: string, apiUrl?: string): AutomaticSkillsInstallResult;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { OxygenError, toFailure } from "@oxygen/shared";
|
|
6
|
+
import { defaultApiUrl, normalizeApiUrl } from "./credentials.js";
|
|
7
|
+
const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
|
|
8
|
+
const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
|
|
9
|
+
export async function listAgentSkills(options) {
|
|
10
|
+
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
11
|
+
const indexUrl = skillIndexUrl(apiUrl);
|
|
12
|
+
const index = await inspectSkillIndex(indexUrl);
|
|
13
|
+
const installed = detectInstalledAgentSkills();
|
|
14
|
+
return {
|
|
15
|
+
index_url: indexUrl,
|
|
16
|
+
available: {
|
|
17
|
+
detected: index.reachable,
|
|
18
|
+
names: index.skill_names,
|
|
19
|
+
skills: index.skills,
|
|
20
|
+
...(index.error ? { error: index.error } : {}),
|
|
21
|
+
},
|
|
22
|
+
installed,
|
|
23
|
+
recommended_install_command: recommendedSkillsInstallCommand(apiUrl),
|
|
24
|
+
install_command_args: recommendedSkillsInstallArgs(apiUrl),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export async function doctorAgentSkills(options) {
|
|
28
|
+
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
29
|
+
const indexUrl = skillIndexUrl(apiUrl);
|
|
30
|
+
const index = await inspectSkillIndex(indexUrl);
|
|
31
|
+
const installer = inspectSkillsInstaller();
|
|
32
|
+
const installed = detectInstalledAgentSkills();
|
|
33
|
+
const data = {
|
|
34
|
+
index_url: indexUrl,
|
|
35
|
+
index,
|
|
36
|
+
installer,
|
|
37
|
+
installed,
|
|
38
|
+
recommended_install_command: recommendedSkillsInstallCommand(apiUrl),
|
|
39
|
+
install_command_args: recommendedSkillsInstallArgs(apiUrl),
|
|
40
|
+
};
|
|
41
|
+
if (!index.reachable) {
|
|
42
|
+
throw new OxygenError("skills_index_unreachable", "Oxygen skill index is not reachable.", {
|
|
43
|
+
details: data,
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (!installer.available) {
|
|
48
|
+
throw new OxygenError("skills_installer_unavailable", "The local npx installer command is not available.", {
|
|
49
|
+
details: data,
|
|
50
|
+
exitCode: 1,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
export function installAgentSkills(options, runtime = {}) {
|
|
56
|
+
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
57
|
+
const indexUrl = skillIndexUrl(apiUrl);
|
|
58
|
+
const agents = readWords(options.agents ?? DEFAULT_SKILL_AGENTS);
|
|
59
|
+
const skill = readOption(options.skill) ?? "*";
|
|
60
|
+
const args = installerAddArgs(indexUrl, agents, skill);
|
|
61
|
+
if (!options.project)
|
|
62
|
+
args.push("--global");
|
|
63
|
+
if (options.copy)
|
|
64
|
+
args.push("--copy");
|
|
65
|
+
let output = "";
|
|
66
|
+
try {
|
|
67
|
+
const exec = runtime.execFileSync ?? execFileSync;
|
|
68
|
+
output = String(exec("npx", args, {
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
env: runtime.env ?? process.env,
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const failure = error;
|
|
76
|
+
throw new OxygenError("skills_install_failed", "Unable to install Oxygen agent skills.", {
|
|
77
|
+
details: {
|
|
78
|
+
index_url: indexUrl,
|
|
79
|
+
stdout: failure.stdout?.slice(0, 2000) ?? "",
|
|
80
|
+
stderr: failure.stderr?.slice(0, 2000) ?? failure.message ?? "unknown",
|
|
81
|
+
},
|
|
82
|
+
exitCode: 1,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
indexUrl,
|
|
87
|
+
index_url: indexUrl,
|
|
88
|
+
agents,
|
|
89
|
+
skill,
|
|
90
|
+
scope: options.project ? "project" : "global",
|
|
91
|
+
output,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function runAutomaticSkillsInstall(options = {}) {
|
|
95
|
+
const apiUrl = normalizeApiUrl(readOption(options.apiUrl) ?? defaultApiUrl());
|
|
96
|
+
const env = options.env ?? process.env;
|
|
97
|
+
if (env.OXYGEN_SKIP_SKILLS === "1") {
|
|
98
|
+
return skippedAutomaticSkillsInstall("OXYGEN_SKIP_SKILLS=1", apiUrl);
|
|
99
|
+
}
|
|
100
|
+
const command = recommendedSkillsInstallCommand(apiUrl);
|
|
101
|
+
try {
|
|
102
|
+
installAgentSkills({
|
|
103
|
+
apiUrl,
|
|
104
|
+
agents: DEFAULT_SKILL_AGENTS,
|
|
105
|
+
skill: "*",
|
|
106
|
+
json: true,
|
|
107
|
+
}, {
|
|
108
|
+
env,
|
|
109
|
+
...(options.execFileSync ? { execFileSync: options.execFileSync } : {}),
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
attempted: true,
|
|
113
|
+
ok: true,
|
|
114
|
+
command,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const failure = toFailure("skills install", error);
|
|
119
|
+
return {
|
|
120
|
+
attempted: true,
|
|
121
|
+
ok: false,
|
|
122
|
+
command,
|
|
123
|
+
error: failure.error,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
export function skippedAutomaticSkillsInstall(reason, apiUrl = normalizeApiUrl(defaultApiUrl())) {
|
|
128
|
+
return {
|
|
129
|
+
attempted: false,
|
|
130
|
+
ok: true,
|
|
131
|
+
command: recommendedSkillsInstallCommand(apiUrl),
|
|
132
|
+
reason,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async function inspectSkillIndex(indexUrl) {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
138
|
+
timeout.unref?.();
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(indexUrl, {
|
|
141
|
+
headers: { Accept: "application/json" },
|
|
142
|
+
signal: controller.signal,
|
|
143
|
+
});
|
|
144
|
+
const text = await response.text();
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
return emptySkillIndexInspection({
|
|
147
|
+
status: response.status,
|
|
148
|
+
error: { code: "http_error", message: `HTTP ${response.status}` },
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(text);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
return emptySkillIndexInspection({
|
|
157
|
+
status: response.status,
|
|
158
|
+
error: {
|
|
159
|
+
code: "invalid_json",
|
|
160
|
+
message: error instanceof Error ? error.message : "Invalid JSON.",
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
let index;
|
|
165
|
+
try {
|
|
166
|
+
index = parseSkillIndex(parsed);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return emptySkillIndexInspection({
|
|
170
|
+
status: response.status,
|
|
171
|
+
error: {
|
|
172
|
+
code: error instanceof OxygenError ? error.code : "invalid_skill_index",
|
|
173
|
+
message: error instanceof Error ? error.message : "Skill index shape is invalid.",
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
reachable: true,
|
|
179
|
+
status: response.status,
|
|
180
|
+
version: index.version,
|
|
181
|
+
base_url: index.base_url,
|
|
182
|
+
skill_names: index.skills.map((skill) => skill.name),
|
|
183
|
+
skills: index.skills,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const aborted = error instanceof Error && error.name === "AbortError";
|
|
188
|
+
return emptySkillIndexInspection({
|
|
189
|
+
status: null,
|
|
190
|
+
error: {
|
|
191
|
+
code: aborted ? "network_timeout" : "network_error",
|
|
192
|
+
message: error instanceof Error ? error.message : "Unable to reach skill index.",
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
clearTimeout(timeout);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function emptySkillIndexInspection(input) {
|
|
201
|
+
return {
|
|
202
|
+
reachable: false,
|
|
203
|
+
status: input.status,
|
|
204
|
+
version: null,
|
|
205
|
+
base_url: null,
|
|
206
|
+
skill_names: [],
|
|
207
|
+
skills: [],
|
|
208
|
+
error: input.error,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function parseSkillIndex(value) {
|
|
212
|
+
if (!isRecord(value) || !Array.isArray(value.skills)) {
|
|
213
|
+
throw new OxygenError("invalid_skill_index", "Skill index must include a skills array.", {
|
|
214
|
+
exitCode: 1,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
const skills = value.skills
|
|
218
|
+
.map((entry) => {
|
|
219
|
+
if (!isRecord(entry) || typeof entry.name !== "string" || !entry.name.trim())
|
|
220
|
+
return null;
|
|
221
|
+
return {
|
|
222
|
+
name: entry.name.trim(),
|
|
223
|
+
title: typeof entry.title === "string" ? entry.title : null,
|
|
224
|
+
description: typeof entry.description === "string" ? entry.description : null,
|
|
225
|
+
entrypoint: typeof entry.entrypoint === "string" ? entry.entrypoint : null,
|
|
226
|
+
};
|
|
227
|
+
})
|
|
228
|
+
.filter((entry) => entry !== null);
|
|
229
|
+
return {
|
|
230
|
+
version: typeof value.version === "string" ? value.version : null,
|
|
231
|
+
base_url: typeof value.base_url === "string" ? value.base_url : null,
|
|
232
|
+
skills,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function inspectSkillsInstaller() {
|
|
236
|
+
const result = spawnSync("npx", ["--version"], {
|
|
237
|
+
encoding: "utf8",
|
|
238
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
239
|
+
timeout: 5000,
|
|
240
|
+
});
|
|
241
|
+
const error = result.error instanceof Error ? result.error.message : null;
|
|
242
|
+
const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
243
|
+
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
|
244
|
+
return {
|
|
245
|
+
command_path: "npx",
|
|
246
|
+
check_args: ["--version"],
|
|
247
|
+
available: result.status === 0 && !error,
|
|
248
|
+
version: stdout || null,
|
|
249
|
+
...(error ? { error } : {}),
|
|
250
|
+
...(stderr ? { stderr: stderr.slice(0, 1000) } : {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function detectInstalledAgentSkills() {
|
|
254
|
+
const roots = [
|
|
255
|
+
{ agent: "codex", path: join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "skills") },
|
|
256
|
+
{ agent: "claude-code", path: join(process.env.CLAUDE_HOME ?? join(homedir(), ".claude"), "skills") },
|
|
257
|
+
{ agent: "cursor", path: join(process.env.CURSOR_HOME ?? join(homedir(), ".cursor"), "skills") },
|
|
258
|
+
{ agent: "project", path: resolve(".agents", "skills") },
|
|
259
|
+
];
|
|
260
|
+
const names = new Set();
|
|
261
|
+
const locations = [];
|
|
262
|
+
for (const root of roots) {
|
|
263
|
+
if (!existsSync(root.path))
|
|
264
|
+
continue;
|
|
265
|
+
try {
|
|
266
|
+
const rootNames = readdirSync(root.path, { withFileTypes: true })
|
|
267
|
+
.filter((entry) => entry.isDirectory() && entry.name.startsWith("oxygen-"))
|
|
268
|
+
.map((entry) => entry.name)
|
|
269
|
+
.sort();
|
|
270
|
+
for (const name of rootNames)
|
|
271
|
+
names.add(name);
|
|
272
|
+
locations.push({ ...root, names: rootNames });
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
locations.push({ ...root, names: [] });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
detected: locations.length > 0,
|
|
280
|
+
names: [...names].sort(),
|
|
281
|
+
locations,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function skillIndexUrl(apiUrl) {
|
|
285
|
+
return `${apiUrl}${SKILL_INDEX_PATH}`;
|
|
286
|
+
}
|
|
287
|
+
function recommendedSkillsInstallCommand(apiUrl) {
|
|
288
|
+
return recommendedSkillsInstallArgs(apiUrl)
|
|
289
|
+
.map((entry) => entry === "*" ? "'*'" : entry)
|
|
290
|
+
.join(" ");
|
|
291
|
+
}
|
|
292
|
+
function recommendedSkillsInstallArgs(apiUrl) {
|
|
293
|
+
const args = ["oxygen", "skills", "install"];
|
|
294
|
+
if (apiUrl !== normalizeApiUrl(defaultApiUrl())) {
|
|
295
|
+
args.push("--api-url", apiUrl);
|
|
296
|
+
}
|
|
297
|
+
args.push("--agents", ...DEFAULT_SKILL_AGENTS, "--skill", "*", "--json");
|
|
298
|
+
return args;
|
|
299
|
+
}
|
|
300
|
+
function installerAddArgs(indexUrl, agents, skill) {
|
|
301
|
+
return [
|
|
302
|
+
"skills",
|
|
303
|
+
"add",
|
|
304
|
+
indexUrl,
|
|
305
|
+
"--agents",
|
|
306
|
+
...agents,
|
|
307
|
+
"--yes",
|
|
308
|
+
"--skill",
|
|
309
|
+
skill,
|
|
310
|
+
"--full-depth",
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
function readWords(value) {
|
|
314
|
+
const text = Array.isArray(value) ? value.join(" ") : value;
|
|
315
|
+
return text
|
|
316
|
+
.split(/[,\s]+/)
|
|
317
|
+
.map((entry) => entry.trim())
|
|
318
|
+
.filter(Boolean);
|
|
319
|
+
}
|
|
320
|
+
function readOption(value) {
|
|
321
|
+
return value?.trim() ? value.trim() : null;
|
|
322
|
+
}
|
|
323
|
+
function isRecord(value) {
|
|
324
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
325
|
+
}
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { type AutomaticSkillsInstallResult, type ExecFileSyncLike } from "./skills.js";
|
|
3
|
+
type SpawnResult = ReturnType<typeof spawnSync>;
|
|
4
|
+
type SpawnSyncLike = (command: string, args: string[], options: {
|
|
5
|
+
encoding: BufferEncoding;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
stdio: "inherit" | ["ignore", "pipe", "pipe"];
|
|
8
|
+
windowsHide: boolean;
|
|
9
|
+
windowsVerbatimArguments?: boolean;
|
|
10
|
+
}) => SpawnResult;
|
|
11
|
+
export type UpdateOptions = {
|
|
12
|
+
json?: boolean;
|
|
13
|
+
package?: string;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type UpdateResult = {
|
|
17
|
+
current_version: string;
|
|
18
|
+
package: string;
|
|
19
|
+
command: string;
|
|
20
|
+
dry_run: boolean;
|
|
21
|
+
updated: boolean;
|
|
22
|
+
skills_install: AutomaticSkillsInstallResult;
|
|
23
|
+
};
|
|
24
|
+
export declare function updateCli(options: UpdateOptions, runtime?: {
|
|
25
|
+
env?: NodeJS.ProcessEnv;
|
|
26
|
+
platform?: NodeJS.Platform;
|
|
27
|
+
modulePath?: string;
|
|
28
|
+
spawnSync?: SpawnSyncLike;
|
|
29
|
+
execFileSync?: ExecFileSyncLike;
|
|
30
|
+
}): UpdateResult;
|
|
31
|
+
export declare function detectCliInstallPrefix(modulePath?: string): string | null;
|
|
32
|
+
export declare function detectCliInstallPrefixFromPath(modulePath: string): string | null;
|
|
33
|
+
export declare function resolveNpmExecutables(platform?: NodeJS.Platform): string[];
|
|
34
|
+
export {};
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { OXYGEN_VERSION, OxygenError } from "@oxygen/shared";
|
|
4
|
+
import { runAutomaticSkillsInstall, skippedAutomaticSkillsInstall, } from "./skills.js";
|
|
5
|
+
const DEFAULT_CLI_PACKAGE_SPEC = "@oxygen-agent/cli@latest";
|
|
6
|
+
// skipcq: JS-R1005 — handles platform/install-mode/dry-run combinations end-to-end
|
|
7
|
+
export function updateCli(options, runtime = {}) {
|
|
8
|
+
const env = runtime.env ?? process.env;
|
|
9
|
+
const platform = runtime.platform ?? process.platform;
|
|
10
|
+
const spawn = runtime.spawnSync ?? spawnSync;
|
|
11
|
+
const packageSpec = readOption(options.package) ?? DEFAULT_CLI_PACKAGE_SPEC;
|
|
12
|
+
const prefix = detectCliInstallPrefix(runtime.modulePath);
|
|
13
|
+
const args = prefix
|
|
14
|
+
? ["install", "-g", "--prefix", prefix, packageSpec]
|
|
15
|
+
: ["install", "-g", packageSpec];
|
|
16
|
+
// The command string is for human display (printed in dry-run / success
|
|
17
|
+
// output). Quote args that contain shell-significant characters so a
|
|
18
|
+
// copy-paste of the line works on install prefixes with spaces.
|
|
19
|
+
const command = ["npm", ...args].map(quoteForDisplay).join(" ");
|
|
20
|
+
if (options.dryRun) {
|
|
21
|
+
return {
|
|
22
|
+
current_version: OXYGEN_VERSION,
|
|
23
|
+
package: packageSpec,
|
|
24
|
+
command,
|
|
25
|
+
dry_run: true,
|
|
26
|
+
updated: false,
|
|
27
|
+
skills_install: skippedAutomaticSkillsInstall("dry_run"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const attemptedExecutables = [];
|
|
31
|
+
let lastResult = null;
|
|
32
|
+
for (const invocation of resolveNpmInvocations(args, platform, env)) {
|
|
33
|
+
attemptedExecutables.push(invocation.label);
|
|
34
|
+
const result = spawn(invocation.executable, invocation.args, {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
env,
|
|
37
|
+
stdio: options.json ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
38
|
+
windowsHide: true,
|
|
39
|
+
// We pre-quote args for `cmd /d /s /c`; Node must not re-escape them
|
|
40
|
+
// or the install prefix path ends up with literal `"` characters
|
|
41
|
+
// baked in (see scripts/ci/cli-package-smoke.mjs for the same fix).
|
|
42
|
+
...(platform === "win32" ? { windowsVerbatimArguments: true } : {}),
|
|
43
|
+
});
|
|
44
|
+
lastResult = result;
|
|
45
|
+
if (result.error && isMissingExecutableError(result.error))
|
|
46
|
+
continue;
|
|
47
|
+
if (!result.error && result.status === 0) {
|
|
48
|
+
return {
|
|
49
|
+
current_version: OXYGEN_VERSION,
|
|
50
|
+
package: packageSpec,
|
|
51
|
+
command,
|
|
52
|
+
dry_run: false,
|
|
53
|
+
updated: true,
|
|
54
|
+
skills_install: runAutomaticSkillsInstall({
|
|
55
|
+
env,
|
|
56
|
+
...(runtime.execFileSync ? { execFileSync: runtime.execFileSync } : {}),
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
const result = lastResult;
|
|
63
|
+
throw new OxygenError("cli_update_failed", "Unable to update the Oxygen CLI.", {
|
|
64
|
+
details: {
|
|
65
|
+
command,
|
|
66
|
+
package: packageSpec,
|
|
67
|
+
exit_code: result?.status ?? null,
|
|
68
|
+
reason: result?.error instanceof Error ? result.error.message : null,
|
|
69
|
+
stderr: typeof result?.stderr === "string" && result.stderr.trim() ? result.stderr.trim().slice(0, 4000) : null,
|
|
70
|
+
attempted_executables: attemptedExecutables,
|
|
71
|
+
},
|
|
72
|
+
exitCode: 1,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function detectCliInstallPrefix(modulePath = fileURLToPath(import.meta.url)) {
|
|
76
|
+
return detectCliInstallPrefixFromPath(modulePath);
|
|
77
|
+
}
|
|
78
|
+
export function detectCliInstallPrefixFromPath(modulePath) {
|
|
79
|
+
const normalizedPath = modulePath.replace(/\\/g, "/");
|
|
80
|
+
const markers = [
|
|
81
|
+
"/node_modules/@oxygen-agent/cli/dist/",
|
|
82
|
+
"/node_modules/@oxygen/cli/dist/",
|
|
83
|
+
];
|
|
84
|
+
for (const marker of markers) {
|
|
85
|
+
const markerIndex = normalizedPath.lastIndexOf(marker);
|
|
86
|
+
if (markerIndex === -1)
|
|
87
|
+
continue;
|
|
88
|
+
const packageParent = normalizedPath.slice(0, markerIndex);
|
|
89
|
+
if (packageParent.endsWith("/lib"))
|
|
90
|
+
return packageParent.slice(0, -"/lib".length);
|
|
91
|
+
return packageParent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
export function resolveNpmExecutables(platform = process.platform) {
|
|
96
|
+
return platform === "win32" ? ["cmd.exe"] : ["npm"];
|
|
97
|
+
}
|
|
98
|
+
function readOption(value) {
|
|
99
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
100
|
+
}
|
|
101
|
+
function isMissingExecutableError(error) {
|
|
102
|
+
return error.code === "ENOENT";
|
|
103
|
+
}
|
|
104
|
+
function resolveNpmInvocations(args, platform, env) {
|
|
105
|
+
if (platform !== "win32")
|
|
106
|
+
return [{ executable: "npm", args, label: "npm" }];
|
|
107
|
+
const shell = env.ComSpec?.trim() || "cmd.exe";
|
|
108
|
+
return [{
|
|
109
|
+
executable: shell,
|
|
110
|
+
args: ["/d", "/s", "/c", ["npm", ...args].map(quoteWindowsCmdArg).join(" ")],
|
|
111
|
+
label: shell,
|
|
112
|
+
}];
|
|
113
|
+
}
|
|
114
|
+
function quoteWindowsCmdArg(value) {
|
|
115
|
+
if (/^[A-Za-z0-9_@+=:,./\\-]+$/.test(value))
|
|
116
|
+
return value;
|
|
117
|
+
return `"${value.replace(/(["^&|<>()%])/g, "^$1")}"`;
|
|
118
|
+
}
|
|
119
|
+
function quoteForDisplay(value) {
|
|
120
|
+
if (/^[A-Za-z0-9_@+=:,./-]+$/.test(value))
|
|
121
|
+
return value;
|
|
122
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
123
|
+
}
|
|
@@ -108,7 +108,8 @@ export declare const ACTIVE_SUBSCRIPTION_STATUSES: readonly ["active", "trialing
|
|
|
108
108
|
export type ActiveSubscriptionStatus = (typeof ACTIVE_SUBSCRIPTION_STATUSES)[number];
|
|
109
109
|
export type ResolvedPricingPlan = PricingPlanDefinition;
|
|
110
110
|
export declare function resolveBasePricingPlan(tier: string | null | undefined): ResolvedPricingPlan | null;
|
|
111
|
-
export declare function applyPricingPlanMetadataOverrides(
|
|
111
|
+
export declare function applyPricingPlanMetadataOverrides(// skipcq: JS-R1005
|
|
112
|
+
plan: PricingPlanDefinition, metadata: PricingPlanMetadata): PricingPlanDefinition;
|
|
112
113
|
export declare function isSelfServePlanTier(value: string): value is SelfServePlanTier;
|
|
113
114
|
export declare function isBillingCurrency(value: string): value is BillingCurrency;
|
|
114
115
|
export declare function normalizeBillingCurrency(value: string | null | undefined): BillingCurrency;
|
|
@@ -208,7 +208,8 @@ export function resolveBasePricingPlan(tier) {
|
|
|
208
208
|
const legacy = LEGACY_PRICING_PLAN_OVERRIDES[tier];
|
|
209
209
|
return legacy ?? null;
|
|
210
210
|
}
|
|
211
|
-
export function applyPricingPlanMetadataOverrides(
|
|
211
|
+
export function applyPricingPlanMetadataOverrides(// skipcq: JS-R1005
|
|
212
|
+
plan, metadata) {
|
|
212
213
|
const record = readRecord(metadata);
|
|
213
214
|
if (!record)
|
|
214
215
|
return plan;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cell display formatter for CLI, MCP widgets, and the web app.
|
|
3
|
+
*
|
|
4
|
+
* One source of truth for how a stored cell value should look to a human.
|
|
5
|
+
* Storage stays untouched — this only decides what to render.
|
|
6
|
+
*
|
|
7
|
+
* Three responsibilities:
|
|
8
|
+
* 1. Format known-typed values consistently (numeric → 1,154; timestamp → ISO/short).
|
|
9
|
+
* 2. Rescue numeric-looking values that landed in a `text` column. Headcount
|
|
10
|
+
* and similar fields routinely arrive as text from CSV imports, AI columns,
|
|
11
|
+
* and provider adapters, so a `text` column whose values parse cleanly as
|
|
12
|
+
* numbers still gets thousands grouping at display time.
|
|
13
|
+
* 3. Refuse to "rescue" things that aren't actually numbers (IP addresses,
|
|
14
|
+
* version strings, phone numbers, URLs), via {@link looksLikeNumericText}.
|
|
15
|
+
*/
|
|
16
|
+
/** Surfaces have different escaping/length budgets, but the canonical string is shared. */
|
|
17
|
+
export type CellFormatSurface = "cli" | "mcp" | "web";
|
|
18
|
+
export type CellColumnLike = {
|
|
19
|
+
dataType?: string | null;
|
|
20
|
+
data_type?: string | null;
|
|
21
|
+
kind?: string | null;
|
|
22
|
+
label?: string | null;
|
|
23
|
+
key?: string | null;
|
|
24
|
+
semanticType?: string | null;
|
|
25
|
+
semantic_type?: string | null;
|
|
26
|
+
};
|
|
27
|
+
export type CellFormatOptions = {
|
|
28
|
+
surface: CellFormatSurface;
|
|
29
|
+
locale?: string;
|
|
30
|
+
/** When the value is rescued from a text column, callers can show a hint. */
|
|
31
|
+
onRescued?: (parsed: number) => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Canonical display string for a single cell.
|
|
35
|
+
*
|
|
36
|
+
* Returns `""` for null/undefined/empty so callers can treat the absence of
|
|
37
|
+
* a value as the absence of a string. Surface-specific concerns (HTML
|
|
38
|
+
* escaping, ellipsis budgets) belong outside this function.
|
|
39
|
+
*/
|
|
40
|
+
export declare function formatCellForDisplay(value: unknown, column: CellColumnLike | null | undefined, options: CellFormatOptions): string;
|
|
41
|
+
/**
|
|
42
|
+
* Best-effort parse of a string that looks numeric (with or without thousands
|
|
43
|
+
* grouping, with or without dotted IP-style separators) into a finite number.
|
|
44
|
+
* Returns `null` when the string is clearly *not* a single number — IP
|
|
45
|
+
* addresses, version strings, phone numbers, anything alphabetic.
|
|
46
|
+
*/
|
|
47
|
+
export declare function rescueNumericText(raw: string): number | null;
|
|
48
|
+
/**
|
|
49
|
+
* True when a string is unambiguously numeric (integer, decimal, grouped, or
|
|
50
|
+
* a mangled dotted form we can recover). Exported for callers that want to
|
|
51
|
+
* highlight rescue cases without re-running the formatter.
|
|
52
|
+
*/
|
|
53
|
+
export declare function looksLikeNumericText(value: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Decide whether the same value-level + column-level guards used inside
|
|
56
|
+
* `formatCellForDisplay`'s text path should rescue this string. Exposed so
|
|
57
|
+
* non-formatter callers (web grid, etc.) can stay in lockstep without
|
|
58
|
+
* duplicating the heuristic.
|
|
59
|
+
*/
|
|
60
|
+
export declare function tryRescueTextCell(value: string, column: CellColumnLike | null | undefined): number | null;
|