ocx 1.4.0 → 1.4.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/dist/index.js +808 -217
- package/dist/index.js.map +14 -13
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9814,6 +9814,120 @@ var coerce = {
|
|
|
9814
9814
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
9815
9815
|
};
|
|
9816
9816
|
var NEVER = INVALID;
|
|
9817
|
+
// src/schemas/registry.ts
|
|
9818
|
+
import { isAbsolute, normalize } from "path";
|
|
9819
|
+
|
|
9820
|
+
// src/utils/errors.ts
|
|
9821
|
+
var EXIT_CODES = {
|
|
9822
|
+
SUCCESS: 0,
|
|
9823
|
+
GENERAL: 1,
|
|
9824
|
+
NOT_FOUND: 66,
|
|
9825
|
+
NETWORK: 69,
|
|
9826
|
+
CONFIG: 78,
|
|
9827
|
+
INTEGRITY: 1
|
|
9828
|
+
};
|
|
9829
|
+
|
|
9830
|
+
class OCXError extends Error {
|
|
9831
|
+
code;
|
|
9832
|
+
exitCode;
|
|
9833
|
+
constructor(message, code, exitCode = EXIT_CODES.GENERAL) {
|
|
9834
|
+
super(message);
|
|
9835
|
+
this.code = code;
|
|
9836
|
+
this.exitCode = exitCode;
|
|
9837
|
+
this.name = "OCXError";
|
|
9838
|
+
}
|
|
9839
|
+
}
|
|
9840
|
+
|
|
9841
|
+
class NotFoundError extends OCXError {
|
|
9842
|
+
constructor(message) {
|
|
9843
|
+
super(message, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
9844
|
+
this.name = "NotFoundError";
|
|
9845
|
+
}
|
|
9846
|
+
}
|
|
9847
|
+
|
|
9848
|
+
class NetworkError extends OCXError {
|
|
9849
|
+
constructor(message) {
|
|
9850
|
+
super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK);
|
|
9851
|
+
this.name = "NetworkError";
|
|
9852
|
+
}
|
|
9853
|
+
}
|
|
9854
|
+
|
|
9855
|
+
class ConfigError extends OCXError {
|
|
9856
|
+
constructor(message) {
|
|
9857
|
+
super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
9858
|
+
this.name = "ConfigError";
|
|
9859
|
+
}
|
|
9860
|
+
}
|
|
9861
|
+
|
|
9862
|
+
class ValidationError extends OCXError {
|
|
9863
|
+
constructor(message) {
|
|
9864
|
+
super(message, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
|
|
9865
|
+
this.name = "ValidationError";
|
|
9866
|
+
}
|
|
9867
|
+
}
|
|
9868
|
+
|
|
9869
|
+
class ConflictError extends OCXError {
|
|
9870
|
+
constructor(message) {
|
|
9871
|
+
super(message, "CONFLICT", EXIT_CODES.GENERAL);
|
|
9872
|
+
this.name = "ConflictError";
|
|
9873
|
+
}
|
|
9874
|
+
}
|
|
9875
|
+
|
|
9876
|
+
class IntegrityError extends OCXError {
|
|
9877
|
+
constructor(component, expected, found) {
|
|
9878
|
+
const message = `Integrity verification failed for "${component}"
|
|
9879
|
+
` + ` Expected: ${expected}
|
|
9880
|
+
` + ` Found: ${found}
|
|
9881
|
+
|
|
9882
|
+
` + `The registry content has changed since this component was locked.
|
|
9883
|
+
` + `Use 'ocx update ${component}' to intentionally update this component.`;
|
|
9884
|
+
super(message, "INTEGRITY_ERROR", EXIT_CODES.INTEGRITY);
|
|
9885
|
+
this.name = "IntegrityError";
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
|
|
9889
|
+
class SelfUpdateError extends OCXError {
|
|
9890
|
+
constructor(message) {
|
|
9891
|
+
super(message, "UPDATE_ERROR", EXIT_CODES.GENERAL);
|
|
9892
|
+
this.name = "SelfUpdateError";
|
|
9893
|
+
}
|
|
9894
|
+
}
|
|
9895
|
+
|
|
9896
|
+
class OcxConfigError extends OCXError {
|
|
9897
|
+
constructor(message) {
|
|
9898
|
+
super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
9899
|
+
this.name = "OcxConfigError";
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
|
|
9903
|
+
class ProfileNotFoundError extends OCXError {
|
|
9904
|
+
constructor(name) {
|
|
9905
|
+
super(`Profile "${name}" not found`, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
9906
|
+
this.name = "ProfileNotFoundError";
|
|
9907
|
+
}
|
|
9908
|
+
}
|
|
9909
|
+
|
|
9910
|
+
class ProfileExistsError extends OCXError {
|
|
9911
|
+
constructor(name) {
|
|
9912
|
+
super(`Profile "${name}" already exists`, "CONFLICT", EXIT_CODES.GENERAL);
|
|
9913
|
+
this.name = "ProfileExistsError";
|
|
9914
|
+
}
|
|
9915
|
+
}
|
|
9916
|
+
|
|
9917
|
+
class InvalidProfileNameError extends OCXError {
|
|
9918
|
+
constructor(name, reason) {
|
|
9919
|
+
super(`Invalid profile name "${name}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
|
|
9920
|
+
this.name = "InvalidProfileNameError";
|
|
9921
|
+
}
|
|
9922
|
+
}
|
|
9923
|
+
|
|
9924
|
+
class ProfilesNotInitializedError extends OCXError {
|
|
9925
|
+
constructor() {
|
|
9926
|
+
super("Profiles not initialized. Run 'ocx profile add default' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
9927
|
+
this.name = "ProfilesNotInitializedError";
|
|
9928
|
+
}
|
|
9929
|
+
}
|
|
9930
|
+
|
|
9817
9931
|
// src/schemas/registry.ts
|
|
9818
9932
|
var npmSpecifierSchema = exports_external.string().refine((val) => val.startsWith("npm:"), {
|
|
9819
9933
|
message: 'npm specifier must start with "npm:" prefix'
|
|
@@ -9860,8 +9974,10 @@ var componentTypeSchema = exports_external.enum([
|
|
|
9860
9974
|
"ocx:plugin",
|
|
9861
9975
|
"ocx:command",
|
|
9862
9976
|
"ocx:tool",
|
|
9863
|
-
"ocx:bundle"
|
|
9977
|
+
"ocx:bundle",
|
|
9978
|
+
"ocx:profile"
|
|
9864
9979
|
]);
|
|
9980
|
+
var profileTargetPathSchema = exports_external.enum(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
|
|
9865
9981
|
var targetPathSchema = exports_external.string().refine((path) => path.startsWith(".opencode/"), {
|
|
9866
9982
|
message: 'Target path must start with ".opencode/"'
|
|
9867
9983
|
}).refine((path) => {
|
|
@@ -9901,7 +10017,7 @@ var mcpServerObjectSchema = exports_external.object({
|
|
|
9901
10017
|
var mcpServerRefSchema = exports_external.union([exports_external.string(), mcpServerObjectSchema]);
|
|
9902
10018
|
var componentFileObjectSchema = exports_external.object({
|
|
9903
10019
|
path: exports_external.string().min(1, "File path cannot be empty"),
|
|
9904
|
-
target:
|
|
10020
|
+
target: exports_external.string().min(1, "Target path cannot be empty")
|
|
9905
10021
|
});
|
|
9906
10022
|
var componentFileSchema = exports_external.union([
|
|
9907
10023
|
exports_external.string().min(1, "File path cannot be empty"),
|
|
@@ -9993,16 +10109,56 @@ var componentManifestSchema = exports_external.object({
|
|
|
9993
10109
|
npmDevDependencies: exports_external.array(exports_external.string()).optional(),
|
|
9994
10110
|
opencode: opencodeConfigSchema.optional()
|
|
9995
10111
|
});
|
|
10112
|
+
function validateSafePath(filePath) {
|
|
10113
|
+
if (isAbsolute(filePath)) {
|
|
10114
|
+
throw new ValidationError(`Invalid path: "${filePath}" - absolute paths not allowed`);
|
|
10115
|
+
}
|
|
10116
|
+
if (filePath.startsWith("~")) {
|
|
10117
|
+
throw new ValidationError(`Invalid path: "${filePath}" - home directory paths not allowed`);
|
|
10118
|
+
}
|
|
10119
|
+
const normalized = normalize(filePath);
|
|
10120
|
+
if (normalized.startsWith("..")) {
|
|
10121
|
+
throw new ValidationError(`Invalid path: "${filePath}" - path traversal not allowed`);
|
|
10122
|
+
}
|
|
10123
|
+
}
|
|
9996
10124
|
function inferTargetPath(sourcePath) {
|
|
9997
10125
|
return `.opencode/${sourcePath}`;
|
|
9998
10126
|
}
|
|
9999
|
-
function
|
|
10127
|
+
function validateFileTarget(target, componentType) {
|
|
10128
|
+
const isProfile = componentType === "ocx:profile";
|
|
10129
|
+
if (isProfile) {
|
|
10130
|
+
const isProfileFile = profileTargetPathSchema.safeParse(target).success;
|
|
10131
|
+
const isOpencodeTarget = target.startsWith(".opencode/");
|
|
10132
|
+
if (!isProfileFile && !isOpencodeTarget) {
|
|
10133
|
+
throw new ValidationError(`Invalid profile target: "${target}". ` + `Must be a profile file (ocx.jsonc, opencode.jsonc, AGENTS.md) or start with ".opencode/"`);
|
|
10134
|
+
}
|
|
10135
|
+
if (isOpencodeTarget) {
|
|
10136
|
+
const parseResult = targetPathSchema.safeParse(target);
|
|
10137
|
+
if (!parseResult.success) {
|
|
10138
|
+
throw new ValidationError(`Invalid embedded target: "${target}". ${parseResult.error.errors[0]?.message}`);
|
|
10139
|
+
}
|
|
10140
|
+
}
|
|
10141
|
+
} else {
|
|
10142
|
+
const parseResult = targetPathSchema.safeParse(target);
|
|
10143
|
+
if (!parseResult.success) {
|
|
10144
|
+
throw new ValidationError(`Invalid target: "${target}". ${parseResult.error.errors[0]?.message}`);
|
|
10145
|
+
}
|
|
10146
|
+
}
|
|
10147
|
+
}
|
|
10148
|
+
function normalizeFile(file, componentType) {
|
|
10149
|
+
const isProfile = componentType === "ocx:profile";
|
|
10000
10150
|
if (typeof file === "string") {
|
|
10151
|
+
validateSafePath(file);
|
|
10152
|
+
const target = isProfile ? file : inferTargetPath(file);
|
|
10153
|
+
validateFileTarget(target, componentType);
|
|
10001
10154
|
return {
|
|
10002
10155
|
path: file,
|
|
10003
|
-
target
|
|
10156
|
+
target
|
|
10004
10157
|
};
|
|
10005
10158
|
}
|
|
10159
|
+
validateSafePath(file.path);
|
|
10160
|
+
validateSafePath(file.target);
|
|
10161
|
+
validateFileTarget(file.target, componentType);
|
|
10006
10162
|
return file;
|
|
10007
10163
|
}
|
|
10008
10164
|
function normalizeMcpServer(server) {
|
|
@@ -10028,7 +10184,7 @@ function normalizeComponentManifest(manifest) {
|
|
|
10028
10184
|
}
|
|
10029
10185
|
return {
|
|
10030
10186
|
...manifest,
|
|
10031
|
-
files: manifest.files.map(normalizeFile),
|
|
10187
|
+
files: manifest.files.map((file) => normalizeFile(file, manifest.type)),
|
|
10032
10188
|
opencode: normalizedOpencode
|
|
10033
10189
|
};
|
|
10034
10190
|
}
|
|
@@ -10128,9 +10284,15 @@ function findOcxConfig(cwd) {
|
|
|
10128
10284
|
}
|
|
10129
10285
|
return { path: dotOpencodePath, exists: false };
|
|
10130
10286
|
}
|
|
10131
|
-
function findOcxLock(cwd) {
|
|
10287
|
+
function findOcxLock(cwd, options) {
|
|
10132
10288
|
const dotOpencodePath = path.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
|
|
10133
10289
|
const rootPath = path.join(cwd, LOCK_FILE);
|
|
10290
|
+
if (options?.isFlattened) {
|
|
10291
|
+
if (existsSync(rootPath)) {
|
|
10292
|
+
return { path: rootPath, exists: true };
|
|
10293
|
+
}
|
|
10294
|
+
return { path: rootPath, exists: false };
|
|
10295
|
+
}
|
|
10134
10296
|
if (existsSync(dotOpencodePath)) {
|
|
10135
10297
|
return { path: dotOpencodePath, exists: true };
|
|
10136
10298
|
}
|
|
@@ -10160,8 +10322,8 @@ async function writeOcxConfig(cwd, config, existingPath) {
|
|
|
10160
10322
|
const content = JSON.stringify(config, null, 2);
|
|
10161
10323
|
await Bun.write(configPath, content);
|
|
10162
10324
|
}
|
|
10163
|
-
async function readOcxLock(cwd) {
|
|
10164
|
-
const { path: lockPath, exists } = findOcxLock(cwd);
|
|
10325
|
+
async function readOcxLock(cwd, options) {
|
|
10326
|
+
const { path: lockPath, exists } = findOcxLock(cwd, options);
|
|
10165
10327
|
if (!exists) {
|
|
10166
10328
|
return null;
|
|
10167
10329
|
}
|
|
@@ -10177,124 +10339,13 @@ async function writeOcxLock(cwd, lock, existingPath) {
|
|
|
10177
10339
|
await Bun.write(lockPath, content);
|
|
10178
10340
|
}
|
|
10179
10341
|
|
|
10180
|
-
// src/utils/errors.ts
|
|
10181
|
-
var EXIT_CODES = {
|
|
10182
|
-
SUCCESS: 0,
|
|
10183
|
-
GENERAL: 1,
|
|
10184
|
-
NOT_FOUND: 66,
|
|
10185
|
-
NETWORK: 69,
|
|
10186
|
-
CONFIG: 78,
|
|
10187
|
-
INTEGRITY: 1
|
|
10188
|
-
};
|
|
10189
|
-
|
|
10190
|
-
class OCXError extends Error {
|
|
10191
|
-
code;
|
|
10192
|
-
exitCode;
|
|
10193
|
-
constructor(message, code, exitCode = EXIT_CODES.GENERAL) {
|
|
10194
|
-
super(message);
|
|
10195
|
-
this.code = code;
|
|
10196
|
-
this.exitCode = exitCode;
|
|
10197
|
-
this.name = "OCXError";
|
|
10198
|
-
}
|
|
10199
|
-
}
|
|
10200
|
-
|
|
10201
|
-
class NotFoundError extends OCXError {
|
|
10202
|
-
constructor(message) {
|
|
10203
|
-
super(message, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
10204
|
-
this.name = "NotFoundError";
|
|
10205
|
-
}
|
|
10206
|
-
}
|
|
10207
|
-
|
|
10208
|
-
class NetworkError extends OCXError {
|
|
10209
|
-
constructor(message) {
|
|
10210
|
-
super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK);
|
|
10211
|
-
this.name = "NetworkError";
|
|
10212
|
-
}
|
|
10213
|
-
}
|
|
10214
|
-
|
|
10215
|
-
class ConfigError extends OCXError {
|
|
10216
|
-
constructor(message) {
|
|
10217
|
-
super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
10218
|
-
this.name = "ConfigError";
|
|
10219
|
-
}
|
|
10220
|
-
}
|
|
10221
|
-
|
|
10222
|
-
class ValidationError extends OCXError {
|
|
10223
|
-
constructor(message) {
|
|
10224
|
-
super(message, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
|
|
10225
|
-
this.name = "ValidationError";
|
|
10226
|
-
}
|
|
10227
|
-
}
|
|
10228
|
-
|
|
10229
|
-
class ConflictError extends OCXError {
|
|
10230
|
-
constructor(message) {
|
|
10231
|
-
super(message, "CONFLICT", EXIT_CODES.GENERAL);
|
|
10232
|
-
this.name = "ConflictError";
|
|
10233
|
-
}
|
|
10234
|
-
}
|
|
10235
|
-
|
|
10236
|
-
class IntegrityError extends OCXError {
|
|
10237
|
-
constructor(component, expected, found) {
|
|
10238
|
-
const message = `Integrity verification failed for "${component}"
|
|
10239
|
-
` + ` Expected: ${expected}
|
|
10240
|
-
` + ` Found: ${found}
|
|
10241
|
-
|
|
10242
|
-
` + `The registry content has changed since this component was locked.
|
|
10243
|
-
` + `Use 'ocx update ${component}' to intentionally update this component.`;
|
|
10244
|
-
super(message, "INTEGRITY_ERROR", EXIT_CODES.INTEGRITY);
|
|
10245
|
-
this.name = "IntegrityError";
|
|
10246
|
-
}
|
|
10247
|
-
}
|
|
10248
|
-
|
|
10249
|
-
class SelfUpdateError extends OCXError {
|
|
10250
|
-
constructor(message) {
|
|
10251
|
-
super(message, "UPDATE_ERROR", EXIT_CODES.GENERAL);
|
|
10252
|
-
this.name = "SelfUpdateError";
|
|
10253
|
-
}
|
|
10254
|
-
}
|
|
10255
|
-
|
|
10256
|
-
class OcxConfigError extends OCXError {
|
|
10257
|
-
constructor(message) {
|
|
10258
|
-
super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
10259
|
-
this.name = "OcxConfigError";
|
|
10260
|
-
}
|
|
10261
|
-
}
|
|
10262
|
-
|
|
10263
|
-
class ProfileNotFoundError extends OCXError {
|
|
10264
|
-
constructor(name) {
|
|
10265
|
-
super(`Profile "${name}" not found`, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
10266
|
-
this.name = "ProfileNotFoundError";
|
|
10267
|
-
}
|
|
10268
|
-
}
|
|
10269
|
-
|
|
10270
|
-
class ProfileExistsError extends OCXError {
|
|
10271
|
-
constructor(name) {
|
|
10272
|
-
super(`Profile "${name}" already exists`, "CONFLICT", EXIT_CODES.GENERAL);
|
|
10273
|
-
this.name = "ProfileExistsError";
|
|
10274
|
-
}
|
|
10275
|
-
}
|
|
10276
|
-
|
|
10277
|
-
class InvalidProfileNameError extends OCXError {
|
|
10278
|
-
constructor(name, reason) {
|
|
10279
|
-
super(`Invalid profile name "${name}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
|
|
10280
|
-
this.name = "InvalidProfileNameError";
|
|
10281
|
-
}
|
|
10282
|
-
}
|
|
10283
|
-
|
|
10284
|
-
class ProfilesNotInitializedError extends OCXError {
|
|
10285
|
-
constructor() {
|
|
10286
|
-
super("Profiles not initialized. Run 'ocx profile add default' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
10287
|
-
this.name = "ProfilesNotInitializedError";
|
|
10288
|
-
}
|
|
10289
|
-
}
|
|
10290
|
-
|
|
10291
10342
|
// src/utils/paths.ts
|
|
10292
10343
|
import { stat } from "fs/promises";
|
|
10293
10344
|
import { homedir } from "os";
|
|
10294
|
-
import { isAbsolute, join } from "path";
|
|
10345
|
+
import { isAbsolute as isAbsolute2, join } from "path";
|
|
10295
10346
|
function getGlobalConfigPath() {
|
|
10296
10347
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
10297
|
-
const base = xdg &&
|
|
10348
|
+
const base = xdg && isAbsolute2(xdg) ? xdg : join(homedir(), ".config");
|
|
10298
10349
|
return join(base, "opencode");
|
|
10299
10350
|
}
|
|
10300
10351
|
async function globalDirectoryExists() {
|
|
@@ -10305,8 +10356,8 @@ async function globalDirectoryExists() {
|
|
|
10305
10356
|
return false;
|
|
10306
10357
|
}
|
|
10307
10358
|
}
|
|
10308
|
-
function resolveTargetPath(target,
|
|
10309
|
-
if (
|
|
10359
|
+
function resolveTargetPath(target, isFlattened) {
|
|
10360
|
+
if (isFlattened && target.startsWith(".opencode/")) {
|
|
10310
10361
|
return target.slice(".opencode/".length);
|
|
10311
10362
|
}
|
|
10312
10363
|
return target;
|
|
@@ -10887,7 +10938,7 @@ class ConfigResolver {
|
|
|
10887
10938
|
// package.json
|
|
10888
10939
|
var package_default = {
|
|
10889
10940
|
name: "ocx",
|
|
10890
|
-
version: "1.4.
|
|
10941
|
+
version: "1.4.1",
|
|
10891
10942
|
description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
|
|
10892
10943
|
author: "kdcokenny",
|
|
10893
10944
|
license: "MIT",
|
|
@@ -11162,8 +11213,19 @@ async function resolveDependencies(registries, componentNames) {
|
|
|
11162
11213
|
// src/updaters/update-opencode-config.ts
|
|
11163
11214
|
import { existsSync as existsSync4 } from "fs";
|
|
11164
11215
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
11216
|
+
import { homedir as homedir3 } from "os";
|
|
11165
11217
|
import path4 from "path";
|
|
11166
11218
|
var LOCAL_CONFIG_DIR3 = ".opencode";
|
|
11219
|
+
function isGlobalConfigPath(cwd) {
|
|
11220
|
+
const base = process.env.XDG_CONFIG_HOME || path4.join(homedir3(), ".config");
|
|
11221
|
+
const globalConfigDir = path4.resolve(base, "opencode");
|
|
11222
|
+
const resolvedCwd = path4.resolve(cwd);
|
|
11223
|
+
if (resolvedCwd === globalConfigDir) {
|
|
11224
|
+
return true;
|
|
11225
|
+
}
|
|
11226
|
+
const relative2 = path4.relative(globalConfigDir, resolvedCwd);
|
|
11227
|
+
return relative2 !== "" && !relative2.startsWith("..") && !path4.isAbsolute(relative2);
|
|
11228
|
+
}
|
|
11167
11229
|
var JSONC_OPTIONS = {
|
|
11168
11230
|
formattingOptions: {
|
|
11169
11231
|
tabSize: 2,
|
|
@@ -11178,6 +11240,17 @@ var OPENCODE_CONFIG_TEMPLATE = `{
|
|
|
11178
11240
|
}
|
|
11179
11241
|
`;
|
|
11180
11242
|
function findOpencodeConfig(cwd) {
|
|
11243
|
+
if (isGlobalConfigPath(cwd)) {
|
|
11244
|
+
const rootJsonc2 = path4.join(cwd, "opencode.jsonc");
|
|
11245
|
+
const rootJson2 = path4.join(cwd, "opencode.json");
|
|
11246
|
+
if (existsSync4(rootJsonc2)) {
|
|
11247
|
+
return { path: rootJsonc2, exists: true };
|
|
11248
|
+
}
|
|
11249
|
+
if (existsSync4(rootJson2)) {
|
|
11250
|
+
return { path: rootJson2, exists: true };
|
|
11251
|
+
}
|
|
11252
|
+
return { path: rootJsonc2, exists: false };
|
|
11253
|
+
}
|
|
11181
11254
|
const dotOpencodeJsonc = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
|
|
11182
11255
|
const dotOpencodeJson = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.json");
|
|
11183
11256
|
if (existsSync4(dotOpencodeJsonc)) {
|
|
@@ -11267,7 +11340,7 @@ async function updateOpencodeJsonConfig(cwd, opencode) {
|
|
|
11267
11340
|
} else {
|
|
11268
11341
|
const config = { $schema: "https://opencode.ai/config.json" };
|
|
11269
11342
|
content = JSON.stringify(config, null, "\t");
|
|
11270
|
-
configPath = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
|
|
11343
|
+
configPath = isGlobalConfigPath(cwd) ? path4.join(cwd, "opencode.jsonc") : path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
|
|
11271
11344
|
await mkdir3(path4.dirname(configPath), { recursive: true });
|
|
11272
11345
|
created = true;
|
|
11273
11346
|
}
|
|
@@ -13182,8 +13255,9 @@ function registerAddCommand(program2) {
|
|
|
13182
13255
|
const resolver = await ConfigResolver.create(options2.cwd ?? process.cwd(), {
|
|
13183
13256
|
profile: options2.profile
|
|
13184
13257
|
});
|
|
13258
|
+
const profileDir = getProfileDir(options2.profile);
|
|
13185
13259
|
provider = {
|
|
13186
|
-
cwd:
|
|
13260
|
+
cwd: profileDir,
|
|
13187
13261
|
getRegistries: () => resolver.getRegistries(),
|
|
13188
13262
|
getComponentPath: () => resolver.getComponentPath()
|
|
13189
13263
|
};
|
|
@@ -13298,10 +13372,11 @@ async function handleNpmPlugins(inputs, options2, cwd) {
|
|
|
13298
13372
|
}
|
|
13299
13373
|
async function runRegistryAddCore(componentNames, options2, provider) {
|
|
13300
13374
|
const cwd = provider.cwd;
|
|
13301
|
-
const
|
|
13375
|
+
const isFlattened = !!options2.global || !!options2.profile;
|
|
13376
|
+
const { path: lockPath } = findOcxLock(cwd, { isFlattened });
|
|
13302
13377
|
const registries = provider.getRegistries();
|
|
13303
13378
|
let lock = { lockVersion: 1, installed: {} };
|
|
13304
|
-
const existingLock = await readOcxLock(cwd);
|
|
13379
|
+
const existingLock = await readOcxLock(cwd, { isFlattened });
|
|
13305
13380
|
if (existingLock) {
|
|
13306
13381
|
lock = existingLock;
|
|
13307
13382
|
}
|
|
@@ -13354,7 +13429,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13354
13429
|
}
|
|
13355
13430
|
const computedHash = await hashBundle(files);
|
|
13356
13431
|
for (const file of component.files) {
|
|
13357
|
-
const targetPath = join3(cwd, resolveTargetPath(file.target,
|
|
13432
|
+
const targetPath = join3(cwd, resolveTargetPath(file.target, isFlattened));
|
|
13358
13433
|
assertPathInside(targetPath, cwd);
|
|
13359
13434
|
}
|
|
13360
13435
|
const existingEntry = lock.installed[component.qualifiedName];
|
|
@@ -13363,7 +13438,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13363
13438
|
throw new IntegrityError(component.qualifiedName, existingEntry.hash, computedHash);
|
|
13364
13439
|
}
|
|
13365
13440
|
for (const file of component.files) {
|
|
13366
|
-
const resolvedTarget = resolveTargetPath(file.target,
|
|
13441
|
+
const resolvedTarget = resolveTargetPath(file.target, isFlattened);
|
|
13367
13442
|
const targetPath = join3(cwd, resolvedTarget);
|
|
13368
13443
|
if (existsSync5(targetPath)) {
|
|
13369
13444
|
const conflictingComponent = findComponentByFile(lock, resolvedTarget);
|
|
@@ -13387,7 +13462,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13387
13462
|
const componentFile = component.files.find((f) => f.path === file.path);
|
|
13388
13463
|
if (!componentFile)
|
|
13389
13464
|
continue;
|
|
13390
|
-
const resolvedTarget = resolveTargetPath(componentFile.target,
|
|
13465
|
+
const resolvedTarget = resolveTargetPath(componentFile.target, isFlattened);
|
|
13391
13466
|
const targetPath = join3(cwd, resolvedTarget);
|
|
13392
13467
|
if (existsSync5(targetPath)) {
|
|
13393
13468
|
const existingContent = await Bun.file(targetPath).text();
|
|
@@ -13415,7 +13490,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13415
13490
|
const installResult = await installComponent(component, files, cwd, {
|
|
13416
13491
|
force: options2.force,
|
|
13417
13492
|
verbose: options2.verbose,
|
|
13418
|
-
|
|
13493
|
+
isFlattened
|
|
13419
13494
|
});
|
|
13420
13495
|
if (options2.verbose) {
|
|
13421
13496
|
for (const f of installResult.skipped) {
|
|
@@ -13436,7 +13511,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13436
13511
|
registry: component.registryName,
|
|
13437
13512
|
version: index.version,
|
|
13438
13513
|
hash: computedHash,
|
|
13439
|
-
files: component.files.map((f) => resolveTargetPath(f.target,
|
|
13514
|
+
files: component.files.map((f) => resolveTargetPath(f.target, isFlattened)),
|
|
13440
13515
|
installedAt: new Date().toISOString()
|
|
13441
13516
|
};
|
|
13442
13517
|
}
|
|
@@ -13453,12 +13528,12 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13453
13528
|
}
|
|
13454
13529
|
const hasNpmDeps = resolved.npmDependencies.length > 0;
|
|
13455
13530
|
const hasNpmDevDeps = resolved.npmDevDependencies.length > 0;
|
|
13456
|
-
const packageJsonPath = options2.global ? join3(cwd, "package.json") : join3(cwd, ".opencode/package.json");
|
|
13531
|
+
const packageJsonPath = options2.global || options2.profile ? join3(cwd, "package.json") : join3(cwd, ".opencode/package.json");
|
|
13457
13532
|
if (hasNpmDeps || hasNpmDevDeps) {
|
|
13458
13533
|
const npmSpin = options2.quiet ? null : createSpinner({ text: `Updating ${packageJsonPath}...` });
|
|
13459
13534
|
npmSpin?.start();
|
|
13460
13535
|
try {
|
|
13461
|
-
await updateOpencodeDevDependencies(cwd, resolved.npmDependencies, resolved.npmDevDependencies, {
|
|
13536
|
+
await updateOpencodeDevDependencies(cwd, resolved.npmDependencies, resolved.npmDevDependencies, { isFlattened });
|
|
13462
13537
|
const totalDeps = resolved.npmDependencies.length + resolved.npmDevDependencies.length;
|
|
13463
13538
|
npmSpin?.succeed(`Added ${totalDeps} dependencies to ${packageJsonPath}`);
|
|
13464
13539
|
} catch (error) {
|
|
@@ -13494,7 +13569,7 @@ async function installComponent(component, files, cwd, options2) {
|
|
|
13494
13569
|
const componentFile = component.files.find((f) => f.path === file.path);
|
|
13495
13570
|
if (!componentFile)
|
|
13496
13571
|
continue;
|
|
13497
|
-
const resolvedTarget = resolveTargetPath(componentFile.target, !!options2.
|
|
13572
|
+
const resolvedTarget = resolveTargetPath(componentFile.target, !!options2.isFlattened);
|
|
13498
13573
|
const targetPath = join3(cwd, resolvedTarget);
|
|
13499
13574
|
const targetDir = dirname(targetPath);
|
|
13500
13575
|
if (existsSync5(targetPath)) {
|
|
@@ -13594,7 +13669,7 @@ async function readOpencodePackageJson(opencodeDir) {
|
|
|
13594
13669
|
return JSON.parse(content2);
|
|
13595
13670
|
} catch (e3) {
|
|
13596
13671
|
const message = e3 instanceof Error ? e3.message : String(e3);
|
|
13597
|
-
throw new ConfigError(`Invalid
|
|
13672
|
+
throw new ConfigError(`Invalid ${pkgPath}: ${message}`);
|
|
13598
13673
|
}
|
|
13599
13674
|
}
|
|
13600
13675
|
async function ensureManifestFilesAreTracked(opencodeDir) {
|
|
@@ -13621,14 +13696,14 @@ async function updateOpencodeDevDependencies(cwd, npmDeps, npmDevDeps, options2
|
|
|
13621
13696
|
const allDepSpecs = [...npmDeps, ...npmDevDeps];
|
|
13622
13697
|
if (allDepSpecs.length === 0)
|
|
13623
13698
|
return;
|
|
13624
|
-
const packageDir = options2.
|
|
13699
|
+
const packageDir = options2.isFlattened ? cwd : join3(cwd, ".opencode");
|
|
13625
13700
|
await mkdir4(packageDir, { recursive: true });
|
|
13626
13701
|
const parsedDeps = allDepSpecs.map(parseNpmDependency);
|
|
13627
13702
|
const existing = await readOpencodePackageJson(packageDir);
|
|
13628
13703
|
const updated = mergeDevDependencies(existing, parsedDeps);
|
|
13629
13704
|
await Bun.write(join3(packageDir, "package.json"), `${JSON.stringify(updated, null, 2)}
|
|
13630
13705
|
`);
|
|
13631
|
-
if (!options2.
|
|
13706
|
+
if (!options2.isFlattened) {
|
|
13632
13707
|
await ensureManifestFilesAreTracked(packageDir);
|
|
13633
13708
|
}
|
|
13634
13709
|
}
|
|
@@ -13689,7 +13764,7 @@ async function buildRegistry(options2) {
|
|
|
13689
13764
|
const packumentPath = join4(componentsDir, `${component.name}.json`);
|
|
13690
13765
|
await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
|
|
13691
13766
|
for (const rawFile of component.files) {
|
|
13692
|
-
const file = normalizeFile(rawFile);
|
|
13767
|
+
const file = normalizeFile(rawFile, component.type);
|
|
13693
13768
|
const sourceFilePath = join4(sourcePath, "files", file.path);
|
|
13694
13769
|
const destFilePath = join4(componentsDir, component.name, file.path);
|
|
13695
13770
|
const destFileDir = dirname2(destFilePath);
|
|
@@ -14851,11 +14926,40 @@ Diff for ${res.name}:`));
|
|
|
14851
14926
|
}
|
|
14852
14927
|
|
|
14853
14928
|
// src/commands/ghost/migrate.ts
|
|
14854
|
-
import {
|
|
14929
|
+
import {
|
|
14930
|
+
copyFileSync,
|
|
14931
|
+
cpSync,
|
|
14932
|
+
existsSync as existsSync7,
|
|
14933
|
+
lstatSync,
|
|
14934
|
+
mkdirSync,
|
|
14935
|
+
readdirSync,
|
|
14936
|
+
renameSync,
|
|
14937
|
+
rmdirSync,
|
|
14938
|
+
rmSync,
|
|
14939
|
+
unlinkSync
|
|
14940
|
+
} from "fs";
|
|
14855
14941
|
import path6 from "path";
|
|
14856
14942
|
var GHOST_CONFIG_FILE = "ghost.jsonc";
|
|
14857
14943
|
var BACKUP_EXT = ".bak";
|
|
14858
14944
|
var CURRENT_SYMLINK = "current";
|
|
14945
|
+
var FLATTEN_DIRS = ["plugin", "agent", "skill", "command"];
|
|
14946
|
+
function moveAtomically(source, destination, isDir) {
|
|
14947
|
+
try {
|
|
14948
|
+
renameSync(source, destination);
|
|
14949
|
+
} catch (err) {
|
|
14950
|
+
if (err instanceof Error && "code" in err && err.code === "EXDEV") {
|
|
14951
|
+
if (isDir) {
|
|
14952
|
+
cpSync(source, destination, { recursive: true });
|
|
14953
|
+
rmSync(source, { recursive: true, force: true });
|
|
14954
|
+
} else {
|
|
14955
|
+
copyFileSync(source, destination);
|
|
14956
|
+
unlinkSync(source);
|
|
14957
|
+
}
|
|
14958
|
+
} else {
|
|
14959
|
+
throw err;
|
|
14960
|
+
}
|
|
14961
|
+
}
|
|
14962
|
+
}
|
|
14859
14963
|
function planMigration() {
|
|
14860
14964
|
const profilesDir = getProfilesDir();
|
|
14861
14965
|
const plan = {
|
|
@@ -14899,6 +15003,55 @@ function planMigration() {
|
|
|
14899
15003
|
backup: backupPath
|
|
14900
15004
|
});
|
|
14901
15005
|
}
|
|
15006
|
+
for (const entry of entries) {
|
|
15007
|
+
if (!entry.isDirectory() || entry.name === CURRENT_SYMLINK)
|
|
15008
|
+
continue;
|
|
15009
|
+
const profileName = entry.name;
|
|
15010
|
+
const profileDir = path6.join(profilesDir, profileName);
|
|
15011
|
+
const dotOpencode = path6.join(profileDir, ".opencode");
|
|
15012
|
+
if (!existsSync7(dotOpencode))
|
|
15013
|
+
continue;
|
|
15014
|
+
for (const dir of FLATTEN_DIRS) {
|
|
15015
|
+
const source = path6.join(dotOpencode, dir);
|
|
15016
|
+
const destination = path6.join(profileDir, dir);
|
|
15017
|
+
if (!existsSync7(source))
|
|
15018
|
+
continue;
|
|
15019
|
+
try {
|
|
15020
|
+
const stat3 = lstatSync(source);
|
|
15021
|
+
if (!stat3.isDirectory()) {
|
|
15022
|
+
plan.skipped.push({
|
|
15023
|
+
profileName,
|
|
15024
|
+
reason: `.opencode/${dir} is not a directory`
|
|
15025
|
+
});
|
|
15026
|
+
continue;
|
|
15027
|
+
}
|
|
15028
|
+
} catch {
|
|
15029
|
+
continue;
|
|
15030
|
+
}
|
|
15031
|
+
if (existsSync7(destination)) {
|
|
15032
|
+
plan.skipped.push({
|
|
15033
|
+
profileName,
|
|
15034
|
+
reason: `${dir}/ exists in both .opencode/ and profile root`
|
|
15035
|
+
});
|
|
15036
|
+
continue;
|
|
15037
|
+
}
|
|
15038
|
+
const backupPath = source + BACKUP_EXT;
|
|
15039
|
+
if (existsSync7(backupPath)) {
|
|
15040
|
+
plan.skipped.push({
|
|
15041
|
+
profileName,
|
|
15042
|
+
reason: `backup already exists: .opencode/${dir}${BACKUP_EXT}`
|
|
15043
|
+
});
|
|
15044
|
+
continue;
|
|
15045
|
+
}
|
|
15046
|
+
plan.profiles.push({
|
|
15047
|
+
type: "move-dir",
|
|
15048
|
+
profileName,
|
|
15049
|
+
source,
|
|
15050
|
+
destination,
|
|
15051
|
+
backup: backupPath
|
|
15052
|
+
});
|
|
15053
|
+
}
|
|
15054
|
+
}
|
|
14902
15055
|
const currentPath = path6.join(profilesDir, CURRENT_SYMLINK);
|
|
14903
15056
|
if (existsSync7(currentPath)) {
|
|
14904
15057
|
try {
|
|
@@ -14924,15 +15077,20 @@ function executeMigration(plan) {
|
|
|
14924
15077
|
const completedRenames = [];
|
|
14925
15078
|
try {
|
|
14926
15079
|
for (const action of plan.profiles) {
|
|
14927
|
-
|
|
14928
|
-
|
|
15080
|
+
if (action.type === "rename") {
|
|
15081
|
+
copyFileSync(action.source, action.backup);
|
|
15082
|
+
} else {
|
|
15083
|
+
cpSync(action.source, action.backup, { recursive: true });
|
|
15084
|
+
}
|
|
15085
|
+
completedBackups.push({ path: action.backup, isDir: action.type === "move-dir" });
|
|
14929
15086
|
}
|
|
14930
15087
|
for (const action of plan.profiles) {
|
|
14931
|
-
|
|
15088
|
+
moveAtomically(action.source, action.destination, action.type === "move-dir");
|
|
14932
15089
|
completedRenames.push({
|
|
14933
15090
|
source: action.source,
|
|
14934
15091
|
destination: action.destination,
|
|
14935
|
-
backup: action.backup
|
|
15092
|
+
backup: action.backup,
|
|
15093
|
+
isDir: action.type === "move-dir"
|
|
14936
15094
|
});
|
|
14937
15095
|
}
|
|
14938
15096
|
if (plan.symlink) {
|
|
@@ -14945,32 +15103,61 @@ function executeMigration(plan) {
|
|
|
14945
15103
|
}
|
|
14946
15104
|
for (const backup of completedBackups) {
|
|
14947
15105
|
try {
|
|
14948
|
-
|
|
15106
|
+
if (backup.isDir) {
|
|
15107
|
+
rmSync(backup.path, { recursive: true, force: true });
|
|
15108
|
+
} else {
|
|
15109
|
+
unlinkSync(backup.path);
|
|
15110
|
+
}
|
|
14949
15111
|
} catch {}
|
|
14950
15112
|
}
|
|
14951
15113
|
} catch (error) {
|
|
14952
15114
|
for (const rename2 of completedRenames) {
|
|
14953
15115
|
try {
|
|
14954
15116
|
if (existsSync7(rename2.destination)) {
|
|
14955
|
-
|
|
15117
|
+
if (rename2.isDir) {
|
|
15118
|
+
rmSync(rename2.destination, { recursive: true, force: true });
|
|
15119
|
+
} else {
|
|
15120
|
+
unlinkSync(rename2.destination);
|
|
15121
|
+
}
|
|
14956
15122
|
}
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
if (
|
|
15123
|
+
mkdirSync(path6.dirname(rename2.source), { recursive: true });
|
|
15124
|
+
if (existsSync7(rename2.backup)) {
|
|
15125
|
+
if (rename2.isDir) {
|
|
15126
|
+
cpSync(rename2.backup, rename2.source, { recursive: true });
|
|
15127
|
+
} else {
|
|
14960
15128
|
copyFileSync(rename2.backup, rename2.source);
|
|
14961
15129
|
}
|
|
14962
|
-
}
|
|
14963
|
-
}
|
|
15130
|
+
}
|
|
15131
|
+
} catch {}
|
|
14964
15132
|
}
|
|
14965
15133
|
for (const backup of completedBackups) {
|
|
14966
15134
|
try {
|
|
14967
|
-
if (existsSync7(backup)) {
|
|
14968
|
-
|
|
15135
|
+
if (existsSync7(backup.path)) {
|
|
15136
|
+
if (backup.isDir) {
|
|
15137
|
+
rmSync(backup.path, { recursive: true, force: true });
|
|
15138
|
+
} else {
|
|
15139
|
+
unlinkSync(backup.path);
|
|
15140
|
+
}
|
|
14969
15141
|
}
|
|
14970
15142
|
} catch {}
|
|
14971
15143
|
}
|
|
14972
15144
|
throw error;
|
|
14973
15145
|
}
|
|
15146
|
+
try {
|
|
15147
|
+
const processedProfiles = new Set(plan.profiles.filter((a) => a.type === "move-dir").map((a) => a.profileName));
|
|
15148
|
+
for (const profileName of processedProfiles) {
|
|
15149
|
+
const profileDir = path6.join(getProfilesDir(), profileName);
|
|
15150
|
+
const dotOpencode = path6.join(profileDir, ".opencode");
|
|
15151
|
+
if (existsSync7(dotOpencode)) {
|
|
15152
|
+
const remaining = readdirSync(dotOpencode);
|
|
15153
|
+
if (remaining.length === 0) {
|
|
15154
|
+
try {
|
|
15155
|
+
rmdirSync(dotOpencode);
|
|
15156
|
+
} catch {}
|
|
15157
|
+
}
|
|
15158
|
+
}
|
|
15159
|
+
}
|
|
15160
|
+
} catch {}
|
|
14974
15161
|
}
|
|
14975
15162
|
function printPlan(plan, dryRun) {
|
|
14976
15163
|
const prefix = dryRun ? "[DRY-RUN] " : "";
|
|
@@ -14983,7 +15170,12 @@ function printPlan(plan, dryRun) {
|
|
|
14983
15170
|
if (plan.profiles.length > 0 || plan.skipped.length > 0) {
|
|
14984
15171
|
console.log("Profiles:");
|
|
14985
15172
|
for (const action of plan.profiles) {
|
|
14986
|
-
|
|
15173
|
+
if (action.type === "rename") {
|
|
15174
|
+
console.log(` \u2713 ${action.profileName}: ${GHOST_CONFIG_FILE} \u2192 ${OCX_CONFIG_FILE}`);
|
|
15175
|
+
} else {
|
|
15176
|
+
const dirName = path6.basename(action.source);
|
|
15177
|
+
console.log(` \u2713 ${action.profileName}: .opencode/${dirName}/ \u2192 ${dirName}/`);
|
|
15178
|
+
}
|
|
14987
15179
|
}
|
|
14988
15180
|
for (const skipped of plan.skipped) {
|
|
14989
15181
|
console.log(` \u26A0 ${skipped.profileName}: skipped (${skipped.reason})`);
|
|
@@ -15389,9 +15581,290 @@ async function runOpencode(pathArg, args, options2) {
|
|
|
15389
15581
|
}
|
|
15390
15582
|
}
|
|
15391
15583
|
|
|
15584
|
+
// src/commands/profile/install-from-registry.ts
|
|
15585
|
+
import { createHash as createHash2 } from "crypto";
|
|
15586
|
+
import { existsSync as existsSync9 } from "fs";
|
|
15587
|
+
import { mkdir as mkdir8, mkdtemp, rename as rename2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
15588
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
15589
|
+
var PROFILE_FILE_TARGETS = new Set(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
|
|
15590
|
+
function isProfileFile(target) {
|
|
15591
|
+
return PROFILE_FILE_TARGETS.has(target);
|
|
15592
|
+
}
|
|
15593
|
+
function hashContent2(content2) {
|
|
15594
|
+
return createHash2("sha256").update(content2).digest("hex");
|
|
15595
|
+
}
|
|
15596
|
+
function hashBundle2(files) {
|
|
15597
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
15598
|
+
const manifestParts = [];
|
|
15599
|
+
for (const file of sorted) {
|
|
15600
|
+
const hash = hashContent2(file.content);
|
|
15601
|
+
manifestParts.push(`${file.path}:${hash}`);
|
|
15602
|
+
}
|
|
15603
|
+
return hashContent2(manifestParts.join(`
|
|
15604
|
+
`));
|
|
15605
|
+
}
|
|
15606
|
+
async function installProfileFromRegistry(options2) {
|
|
15607
|
+
const { namespace, component, profileName, force, registryUrl, registries, quiet } = options2;
|
|
15608
|
+
const parseResult = profileNameSchema.safeParse(profileName);
|
|
15609
|
+
if (!parseResult.success) {
|
|
15610
|
+
throw new ValidationError(`Invalid profile name: "${profileName}". ` + `Profile names must start with a letter and contain only alphanumeric characters, dots, underscores, or hyphens.`);
|
|
15611
|
+
}
|
|
15612
|
+
const profileDir = getProfileDir(profileName);
|
|
15613
|
+
const qualifiedName = `${namespace}/${component}`;
|
|
15614
|
+
const profileExists = existsSync9(profileDir);
|
|
15615
|
+
if (profileExists && !force) {
|
|
15616
|
+
throw new ConflictError(`Profile "${profileName}" already exists.
|
|
15617
|
+
Use --force to overwrite.`);
|
|
15618
|
+
}
|
|
15619
|
+
const fetchSpin = quiet ? null : createSpinner({ text: `Fetching ${qualifiedName}...` });
|
|
15620
|
+
fetchSpin?.start();
|
|
15621
|
+
let manifest;
|
|
15622
|
+
try {
|
|
15623
|
+
manifest = await fetchComponent(registryUrl, component);
|
|
15624
|
+
} catch (error) {
|
|
15625
|
+
fetchSpin?.fail(`Failed to fetch ${qualifiedName}`);
|
|
15626
|
+
if (error instanceof NotFoundError) {
|
|
15627
|
+
throw new NotFoundError(`Profile component "${qualifiedName}" not found in registry.
|
|
15628
|
+
|
|
15629
|
+
` + `Check the component name and ensure the registry is configured.`);
|
|
15630
|
+
}
|
|
15631
|
+
throw error;
|
|
15632
|
+
}
|
|
15633
|
+
if (manifest.type !== "ocx:profile") {
|
|
15634
|
+
fetchSpin?.fail(`Invalid component type`);
|
|
15635
|
+
throw new ValidationError(`Component "${qualifiedName}" is type "${manifest.type}", not "ocx:profile".
|
|
15636
|
+
|
|
15637
|
+
` + `Only profile components can be installed with 'ocx profile add --from'.`);
|
|
15638
|
+
}
|
|
15639
|
+
const normalized = normalizeComponentManifest(manifest);
|
|
15640
|
+
fetchSpin?.succeed(`Fetched ${qualifiedName}`);
|
|
15641
|
+
const filesSpin = quiet ? null : createSpinner({ text: "Downloading profile files..." });
|
|
15642
|
+
filesSpin?.start();
|
|
15643
|
+
const profileFiles = [];
|
|
15644
|
+
const dependencyFiles = [];
|
|
15645
|
+
for (const file of normalized.files) {
|
|
15646
|
+
const content2 = await fetchFileContent(registryUrl, component, file.path);
|
|
15647
|
+
const fileEntry = {
|
|
15648
|
+
path: file.path,
|
|
15649
|
+
target: file.target,
|
|
15650
|
+
content: Buffer.from(content2)
|
|
15651
|
+
};
|
|
15652
|
+
if (isProfileFile(file.target)) {
|
|
15653
|
+
profileFiles.push(fileEntry);
|
|
15654
|
+
} else {
|
|
15655
|
+
dependencyFiles.push(fileEntry);
|
|
15656
|
+
}
|
|
15657
|
+
}
|
|
15658
|
+
filesSpin?.succeed(`Downloaded ${normalized.files.length} files`);
|
|
15659
|
+
let resolvedDeps = null;
|
|
15660
|
+
const dependencyBundles = [];
|
|
15661
|
+
if (manifest.dependencies.length > 0) {
|
|
15662
|
+
const depsSpin = quiet ? null : createSpinner({ text: "Resolving dependencies..." });
|
|
15663
|
+
depsSpin?.start();
|
|
15664
|
+
try {
|
|
15665
|
+
const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
|
|
15666
|
+
resolvedDeps = await resolveDependencies(registries, depRefs);
|
|
15667
|
+
for (const depComponent of resolvedDeps.components) {
|
|
15668
|
+
const files = [];
|
|
15669
|
+
for (const file of depComponent.files) {
|
|
15670
|
+
const content2 = await fetchFileContent(depComponent.baseUrl, depComponent.name, file.path);
|
|
15671
|
+
const resolvedTarget = resolveTargetPath(file.target, true);
|
|
15672
|
+
files.push({
|
|
15673
|
+
path: file.path,
|
|
15674
|
+
target: resolvedTarget,
|
|
15675
|
+
content: Buffer.from(content2)
|
|
15676
|
+
});
|
|
15677
|
+
}
|
|
15678
|
+
const registryIndex = await fetchRegistryIndex(depComponent.baseUrl);
|
|
15679
|
+
dependencyBundles.push({
|
|
15680
|
+
qualifiedName: depComponent.qualifiedName,
|
|
15681
|
+
registryName: depComponent.registryName,
|
|
15682
|
+
files,
|
|
15683
|
+
hash: hashBundle2(files),
|
|
15684
|
+
version: registryIndex.version
|
|
15685
|
+
});
|
|
15686
|
+
}
|
|
15687
|
+
depsSpin?.succeed(`Resolved ${resolvedDeps.components.length} dependencies`);
|
|
15688
|
+
} catch (error) {
|
|
15689
|
+
depsSpin?.fail("Failed to resolve dependencies");
|
|
15690
|
+
throw error;
|
|
15691
|
+
}
|
|
15692
|
+
}
|
|
15693
|
+
const profilesDir = getProfilesDir();
|
|
15694
|
+
await mkdir8(profilesDir, { recursive: true, mode: 448 });
|
|
15695
|
+
const stagingDir = await mkdtemp(join8(profilesDir, ".staging-"));
|
|
15696
|
+
const stagingOpencodeDir = join8(stagingDir, ".opencode");
|
|
15697
|
+
try {
|
|
15698
|
+
await mkdir8(stagingOpencodeDir, { recursive: true, mode: 448 });
|
|
15699
|
+
const writeSpin = quiet ? null : createSpinner({ text: "Writing profile files..." });
|
|
15700
|
+
writeSpin?.start();
|
|
15701
|
+
for (const file of profileFiles) {
|
|
15702
|
+
const targetPath = join8(stagingDir, file.target);
|
|
15703
|
+
const targetDir = dirname4(targetPath);
|
|
15704
|
+
if (!existsSync9(targetDir)) {
|
|
15705
|
+
await mkdir8(targetDir, { recursive: true });
|
|
15706
|
+
}
|
|
15707
|
+
await writeFile3(targetPath, file.content);
|
|
15708
|
+
}
|
|
15709
|
+
for (const file of dependencyFiles) {
|
|
15710
|
+
const target = file.target.startsWith(".opencode/") ? file.target.slice(".opencode/".length) : file.target;
|
|
15711
|
+
const targetPath = join8(stagingOpencodeDir, target);
|
|
15712
|
+
const targetDir = dirname4(targetPath);
|
|
15713
|
+
if (!existsSync9(targetDir)) {
|
|
15714
|
+
await mkdir8(targetDir, { recursive: true });
|
|
15715
|
+
}
|
|
15716
|
+
await writeFile3(targetPath, file.content);
|
|
15717
|
+
}
|
|
15718
|
+
writeSpin?.succeed(`Wrote ${profileFiles.length + dependencyFiles.length} profile files`);
|
|
15719
|
+
if (dependencyBundles.length > 0) {
|
|
15720
|
+
const depWriteSpin = quiet ? null : createSpinner({ text: "Writing dependency files..." });
|
|
15721
|
+
depWriteSpin?.start();
|
|
15722
|
+
let depFileCount = 0;
|
|
15723
|
+
for (const bundle of dependencyBundles) {
|
|
15724
|
+
for (const file of bundle.files) {
|
|
15725
|
+
const targetPath = join8(stagingOpencodeDir, file.target);
|
|
15726
|
+
const targetDir = dirname4(targetPath);
|
|
15727
|
+
if (!existsSync9(targetDir)) {
|
|
15728
|
+
await mkdir8(targetDir, { recursive: true });
|
|
15729
|
+
}
|
|
15730
|
+
await writeFile3(targetPath, file.content);
|
|
15731
|
+
depFileCount++;
|
|
15732
|
+
}
|
|
15733
|
+
}
|
|
15734
|
+
depWriteSpin?.succeed(`Wrote ${depFileCount} dependency files`);
|
|
15735
|
+
}
|
|
15736
|
+
const profileHash = hashBundle2(profileFiles.map((f) => ({ path: f.path, content: f.content })));
|
|
15737
|
+
const registryIndex = await fetchRegistryIndex(registryUrl);
|
|
15738
|
+
const lock = {
|
|
15739
|
+
version: 1,
|
|
15740
|
+
installedFrom: {
|
|
15741
|
+
registry: namespace,
|
|
15742
|
+
component,
|
|
15743
|
+
version: registryIndex.version,
|
|
15744
|
+
hash: profileHash,
|
|
15745
|
+
installedAt: new Date().toISOString()
|
|
15746
|
+
},
|
|
15747
|
+
installed: {}
|
|
15748
|
+
};
|
|
15749
|
+
for (const bundle of dependencyBundles) {
|
|
15750
|
+
lock.installed[bundle.qualifiedName] = {
|
|
15751
|
+
registry: bundle.registryName,
|
|
15752
|
+
version: bundle.version,
|
|
15753
|
+
hash: bundle.hash,
|
|
15754
|
+
files: bundle.files.map((f) => f.target),
|
|
15755
|
+
installedAt: new Date().toISOString()
|
|
15756
|
+
};
|
|
15757
|
+
}
|
|
15758
|
+
await writeFile3(join8(stagingDir, "ocx.lock"), JSON.stringify(lock, null, "\t"));
|
|
15759
|
+
const moveSpin = quiet ? null : createSpinner({ text: "Finalizing installation..." });
|
|
15760
|
+
moveSpin?.start();
|
|
15761
|
+
const profilesDir2 = dirname4(profileDir);
|
|
15762
|
+
if (!existsSync9(profilesDir2)) {
|
|
15763
|
+
await mkdir8(profilesDir2, { recursive: true, mode: 448 });
|
|
15764
|
+
}
|
|
15765
|
+
if (profileExists && force) {
|
|
15766
|
+
const backupDir = `${profileDir}.backup-${Date.now()}`;
|
|
15767
|
+
await rename2(profileDir, backupDir);
|
|
15768
|
+
try {
|
|
15769
|
+
await rename2(stagingDir, profileDir);
|
|
15770
|
+
} catch (err) {
|
|
15771
|
+
await rename2(backupDir, profileDir);
|
|
15772
|
+
throw err;
|
|
15773
|
+
}
|
|
15774
|
+
await rm3(backupDir, { recursive: true, force: true });
|
|
15775
|
+
} else {
|
|
15776
|
+
await rename2(stagingDir, profileDir);
|
|
15777
|
+
}
|
|
15778
|
+
moveSpin?.succeed("Installation complete");
|
|
15779
|
+
if (!quiet) {
|
|
15780
|
+
logger.info("");
|
|
15781
|
+
logger.success(`Installed profile "${profileName}" from ${qualifiedName}`);
|
|
15782
|
+
logger.info("");
|
|
15783
|
+
logger.info("Profile contents:");
|
|
15784
|
+
for (const file of profileFiles) {
|
|
15785
|
+
logger.info(` ${file.target}`);
|
|
15786
|
+
}
|
|
15787
|
+
if (dependencyBundles.length > 0) {
|
|
15788
|
+
logger.info("");
|
|
15789
|
+
logger.info("Dependencies:");
|
|
15790
|
+
for (const bundle of dependencyBundles) {
|
|
15791
|
+
logger.info(` ${bundle.qualifiedName}`);
|
|
15792
|
+
}
|
|
15793
|
+
}
|
|
15794
|
+
}
|
|
15795
|
+
} catch (error) {
|
|
15796
|
+
try {
|
|
15797
|
+
if (existsSync9(stagingDir)) {
|
|
15798
|
+
await rm3(stagingDir, { recursive: true });
|
|
15799
|
+
}
|
|
15800
|
+
} catch {}
|
|
15801
|
+
throw error;
|
|
15802
|
+
}
|
|
15803
|
+
}
|
|
15804
|
+
|
|
15392
15805
|
// src/commands/profile/add.ts
|
|
15806
|
+
function parseFromOption(from) {
|
|
15807
|
+
if (!from?.trim()) {
|
|
15808
|
+
throw new ValidationError("--from value cannot be empty");
|
|
15809
|
+
}
|
|
15810
|
+
const trimmed = from.trim();
|
|
15811
|
+
if (trimmed.startsWith("./") || trimmed.startsWith("~/") || trimmed.startsWith("/")) {
|
|
15812
|
+
return { type: "local-path", path: trimmed };
|
|
15813
|
+
}
|
|
15814
|
+
const slashCount = (trimmed.match(/\//g) || []).length;
|
|
15815
|
+
if (slashCount === 1) {
|
|
15816
|
+
const [namespace, component] = trimmed.split("/").map((s) => s.trim());
|
|
15817
|
+
if (!namespace || !component) {
|
|
15818
|
+
throw new ValidationError(`Invalid registry reference: "${from}". Expected format: namespace/component`);
|
|
15819
|
+
}
|
|
15820
|
+
return { type: "registry", namespace, component };
|
|
15821
|
+
}
|
|
15822
|
+
return { type: "local-profile", name: trimmed };
|
|
15823
|
+
}
|
|
15824
|
+
async function readGlobalOcxConfig() {
|
|
15825
|
+
const configPath = getGlobalConfig();
|
|
15826
|
+
const file = Bun.file(configPath);
|
|
15827
|
+
if (!await file.exists()) {
|
|
15828
|
+
return null;
|
|
15829
|
+
}
|
|
15830
|
+
try {
|
|
15831
|
+
const content2 = await file.text();
|
|
15832
|
+
const json = parse2(content2, [], { allowTrailingComma: true });
|
|
15833
|
+
return profileOcxConfigSchema.parse(json);
|
|
15834
|
+
} catch (error) {
|
|
15835
|
+
if (error instanceof Error) {
|
|
15836
|
+
throw new ConfigError(`Failed to parse global config at "${configPath}": ${error.message}
|
|
15837
|
+
|
|
15838
|
+
` + `Check the file for syntax errors or run: ocx config edit --global`);
|
|
15839
|
+
}
|
|
15840
|
+
throw error;
|
|
15841
|
+
}
|
|
15842
|
+
}
|
|
15843
|
+
async function requireGlobalRegistry(namespace) {
|
|
15844
|
+
const globalConfig = await readGlobalOcxConfig();
|
|
15845
|
+
if (!globalConfig) {
|
|
15846
|
+
throw new ConfigError(`Registry "${namespace}" is not configured globally.
|
|
15847
|
+
|
|
15848
|
+
` + `Profile installation requires global registry configuration.
|
|
15849
|
+
` + `Run: ocx registry add ${namespace} <url> --global`);
|
|
15850
|
+
}
|
|
15851
|
+
const registry = globalConfig.registries[namespace];
|
|
15852
|
+
if (!registry) {
|
|
15853
|
+
throw new ConfigError(`Registry "${namespace}" is not configured globally.
|
|
15854
|
+
|
|
15855
|
+
` + `Profile installation requires global registry configuration.
|
|
15856
|
+
` + `Run: ocx registry add ${namespace} <url> --global`);
|
|
15857
|
+
}
|
|
15858
|
+
return { config: globalConfig, registryUrl: registry.url };
|
|
15859
|
+
}
|
|
15393
15860
|
function registerProfileAddCommand(parent) {
|
|
15394
|
-
parent.command("add <name>").description("Create a new
|
|
15861
|
+
parent.command("add <name>").description("Create a new profile, clone from existing, or install from registry").option("--from <source>", "Clone from existing profile or install from registry (e.g., kdco/minimal)").option("-f, --force", "Overwrite existing profile").addHelpText("after", `
|
|
15862
|
+
Examples:
|
|
15863
|
+
$ ocx profile add work # Create empty profile
|
|
15864
|
+
$ ocx profile add work --from dev # Clone from existing profile
|
|
15865
|
+
$ ocx profile add work --from kdco/minimal # Install from registry
|
|
15866
|
+
$ ocx profile add work --from kdco/minimal --force # Overwrite existing
|
|
15867
|
+
`).action(async (name, options2) => {
|
|
15395
15868
|
try {
|
|
15396
15869
|
await runProfileAdd(name, options2);
|
|
15397
15870
|
} catch (error) {
|
|
@@ -15401,15 +15874,61 @@ function registerProfileAddCommand(parent) {
|
|
|
15401
15874
|
}
|
|
15402
15875
|
async function runProfileAdd(name, options2) {
|
|
15403
15876
|
const manager = await ProfileManager.requireInitialized();
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
logger.
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
15877
|
+
const profileExists = await manager.exists(name);
|
|
15878
|
+
if (profileExists && !options2.force) {
|
|
15879
|
+
logger.error(`\u2717 Profile "${name}" already exists`);
|
|
15880
|
+
logger.error("");
|
|
15881
|
+
logger.error("Use --force to overwrite the existing profile.");
|
|
15882
|
+
throw new ProfileExistsError(name);
|
|
15883
|
+
}
|
|
15884
|
+
if (!options2.from) {
|
|
15885
|
+
await createEmptyProfile(manager, name, profileExists);
|
|
15886
|
+
return;
|
|
15412
15887
|
}
|
|
15888
|
+
const fromInput = parseFromOption(options2.from);
|
|
15889
|
+
switch (fromInput.type) {
|
|
15890
|
+
case "local-profile":
|
|
15891
|
+
await cloneFromLocalProfile(manager, name, fromInput.name, profileExists);
|
|
15892
|
+
break;
|
|
15893
|
+
case "local-path":
|
|
15894
|
+
throw new ValidationError(`Local path installation is not yet implemented: "${fromInput.path}"
|
|
15895
|
+
|
|
15896
|
+
` + `Currently supported sources:
|
|
15897
|
+
` + ` - Existing profile: --from <profile-name>
|
|
15898
|
+
` + ` - Registry: --from <namespace>/<component>`);
|
|
15899
|
+
case "registry": {
|
|
15900
|
+
const { config: globalConfig, registryUrl } = await requireGlobalRegistry(fromInput.namespace);
|
|
15901
|
+
const registries = {};
|
|
15902
|
+
for (const [ns, reg] of Object.entries(globalConfig.registries)) {
|
|
15903
|
+
registries[ns] = { url: reg.url };
|
|
15904
|
+
}
|
|
15905
|
+
await installProfileFromRegistry({
|
|
15906
|
+
namespace: fromInput.namespace,
|
|
15907
|
+
component: fromInput.component,
|
|
15908
|
+
profileName: name,
|
|
15909
|
+
force: options2.force,
|
|
15910
|
+
registryUrl,
|
|
15911
|
+
registries
|
|
15912
|
+
});
|
|
15913
|
+
break;
|
|
15914
|
+
}
|
|
15915
|
+
}
|
|
15916
|
+
}
|
|
15917
|
+
async function createEmptyProfile(manager, name, exists) {
|
|
15918
|
+
if (exists) {
|
|
15919
|
+
await manager.remove(name);
|
|
15920
|
+
}
|
|
15921
|
+
await manager.add(name);
|
|
15922
|
+
logger.success(`Created profile "${name}"`);
|
|
15923
|
+
}
|
|
15924
|
+
async function cloneFromLocalProfile(manager, name, sourceName, exists) {
|
|
15925
|
+
const source = await manager.get(sourceName);
|
|
15926
|
+
if (exists) {
|
|
15927
|
+
await manager.remove(name);
|
|
15928
|
+
}
|
|
15929
|
+
await manager.add(name);
|
|
15930
|
+
await atomicWrite(getProfileOcxConfig(name), source.ocx);
|
|
15931
|
+
logger.success(`Created profile "${name}" (cloned from "${sourceName}")`);
|
|
15413
15932
|
}
|
|
15414
15933
|
|
|
15415
15934
|
// src/commands/profile/config.ts
|
|
@@ -15566,31 +16085,56 @@ function runRegistryListCore(callbacks) {
|
|
|
15566
16085
|
function registerRegistryCommand(program2) {
|
|
15567
16086
|
const registry = program2.command("registry").description("Manage registries");
|
|
15568
16087
|
const addCmd = registry.command("add").description("Add a registry").argument("<url>", "Registry URL").option("--name <name>", "Registry alias (defaults to hostname)").option("--version <version>", "Pin to specific version");
|
|
16088
|
+
addGlobalOption(addCmd);
|
|
15569
16089
|
addCommonOptions(addCmd);
|
|
15570
|
-
addCmd.action(async (url, options2) => {
|
|
16090
|
+
addCmd.action(async (url, options2, command) => {
|
|
15571
16091
|
try {
|
|
15572
|
-
const
|
|
15573
|
-
|
|
15574
|
-
|
|
15575
|
-
if (!config) {
|
|
15576
|
-
logger.error("No ocx.jsonc found. Run 'ocx init' first.");
|
|
16092
|
+
const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
|
|
16093
|
+
if (options2.global && cwdExplicitlyProvided) {
|
|
16094
|
+
logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
|
|
15577
16095
|
process.exit(1);
|
|
15578
16096
|
}
|
|
16097
|
+
let configDir;
|
|
16098
|
+
let configPath;
|
|
16099
|
+
const config = await (async () => {
|
|
16100
|
+
if (options2.global) {
|
|
16101
|
+
configDir = getGlobalConfigPath();
|
|
16102
|
+
const found = findOcxConfig(configDir);
|
|
16103
|
+
configPath = found.path;
|
|
16104
|
+
const cfg = await readOcxConfig(configDir);
|
|
16105
|
+
if (!cfg) {
|
|
16106
|
+
logger.error("Global config not found. Run 'opencode' once to initialize.");
|
|
16107
|
+
process.exit(1);
|
|
16108
|
+
}
|
|
16109
|
+
return cfg;
|
|
16110
|
+
} else {
|
|
16111
|
+
configDir = options2.cwd ?? process.cwd();
|
|
16112
|
+
const found = findOcxConfig(configDir);
|
|
16113
|
+
configPath = found.path;
|
|
16114
|
+
const cfg = await readOcxConfig(configDir);
|
|
16115
|
+
if (!cfg) {
|
|
16116
|
+
logger.error("No ocx.jsonc found. Run 'ocx init' first.");
|
|
16117
|
+
process.exit(1);
|
|
16118
|
+
}
|
|
16119
|
+
return cfg;
|
|
16120
|
+
}
|
|
16121
|
+
})();
|
|
15579
16122
|
const result = await runRegistryAddCore2(url, options2, {
|
|
15580
16123
|
getRegistries: () => config.registries,
|
|
15581
16124
|
isLocked: () => config.lockRegistries ?? false,
|
|
15582
16125
|
setRegistry: async (name, regConfig) => {
|
|
15583
16126
|
config.registries[name] = regConfig;
|
|
15584
|
-
await writeOcxConfig(
|
|
16127
|
+
await writeOcxConfig(configDir, config, configPath);
|
|
15585
16128
|
}
|
|
15586
16129
|
});
|
|
15587
16130
|
if (options2.json) {
|
|
15588
16131
|
outputJson({ success: true, data: result });
|
|
15589
16132
|
} else if (!options2.quiet) {
|
|
16133
|
+
const location = options2.global ? "global config" : "local config";
|
|
15590
16134
|
if (result.updated) {
|
|
15591
|
-
logger.success(`Updated registry: ${result.name} -> ${result.url}`);
|
|
16135
|
+
logger.success(`Updated registry in ${location}: ${result.name} -> ${result.url}`);
|
|
15592
16136
|
} else {
|
|
15593
|
-
logger.success(`Added registry: ${result.name} -> ${result.url}`);
|
|
16137
|
+
logger.success(`Added registry to ${location}: ${result.name} -> ${result.url}`);
|
|
15594
16138
|
}
|
|
15595
16139
|
}
|
|
15596
16140
|
} catch (error) {
|
|
@@ -15598,43 +16142,90 @@ function registerRegistryCommand(program2) {
|
|
|
15598
16142
|
}
|
|
15599
16143
|
});
|
|
15600
16144
|
const removeCmd = registry.command("remove").description("Remove a registry").argument("<name>", "Registry name");
|
|
16145
|
+
addGlobalOption(removeCmd);
|
|
15601
16146
|
addCommonOptions(removeCmd);
|
|
15602
|
-
removeCmd.action(async (name, options2) => {
|
|
16147
|
+
removeCmd.action(async (name, options2, command) => {
|
|
15603
16148
|
try {
|
|
15604
|
-
const
|
|
15605
|
-
|
|
15606
|
-
|
|
15607
|
-
if (!config) {
|
|
15608
|
-
logger.error("No ocx.jsonc found. Run 'ocx init' first.");
|
|
16149
|
+
const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
|
|
16150
|
+
if (options2.global && cwdExplicitlyProvided) {
|
|
16151
|
+
logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
|
|
15609
16152
|
process.exit(1);
|
|
15610
16153
|
}
|
|
16154
|
+
let configDir;
|
|
16155
|
+
let configPath;
|
|
16156
|
+
const config = await (async () => {
|
|
16157
|
+
if (options2.global) {
|
|
16158
|
+
configDir = getGlobalConfigPath();
|
|
16159
|
+
const found = findOcxConfig(configDir);
|
|
16160
|
+
configPath = found.path;
|
|
16161
|
+
const cfg = await readOcxConfig(configDir);
|
|
16162
|
+
if (!cfg) {
|
|
16163
|
+
logger.error("Global config not found. Run 'opencode' once to initialize.");
|
|
16164
|
+
process.exit(1);
|
|
16165
|
+
}
|
|
16166
|
+
return cfg;
|
|
16167
|
+
} else {
|
|
16168
|
+
configDir = options2.cwd ?? process.cwd();
|
|
16169
|
+
const found = findOcxConfig(configDir);
|
|
16170
|
+
configPath = found.path;
|
|
16171
|
+
const cfg = await readOcxConfig(configDir);
|
|
16172
|
+
if (!cfg) {
|
|
16173
|
+
logger.error("No ocx.jsonc found. Run 'ocx init' first.");
|
|
16174
|
+
process.exit(1);
|
|
16175
|
+
}
|
|
16176
|
+
return cfg;
|
|
16177
|
+
}
|
|
16178
|
+
})();
|
|
15611
16179
|
const result = await runRegistryRemoveCore(name, {
|
|
15612
16180
|
getRegistries: () => config.registries,
|
|
15613
16181
|
isLocked: () => config.lockRegistries ?? false,
|
|
15614
16182
|
removeRegistry: async (regName) => {
|
|
15615
16183
|
delete config.registries[regName];
|
|
15616
|
-
await writeOcxConfig(
|
|
16184
|
+
await writeOcxConfig(configDir, config, configPath);
|
|
15617
16185
|
}
|
|
15618
16186
|
});
|
|
15619
16187
|
if (options2.json) {
|
|
15620
16188
|
outputJson({ success: true, data: result });
|
|
15621
16189
|
} else if (!options2.quiet) {
|
|
15622
|
-
|
|
16190
|
+
const location = options2.global ? "global config" : "local config";
|
|
16191
|
+
logger.success(`Removed registry from ${location}: ${result.removed}`);
|
|
15623
16192
|
}
|
|
15624
16193
|
} catch (error) {
|
|
15625
16194
|
handleError(error, { json: options2.json });
|
|
15626
16195
|
}
|
|
15627
16196
|
});
|
|
15628
16197
|
const listCmd = registry.command("list").description("List configured registries");
|
|
16198
|
+
addGlobalOption(listCmd);
|
|
15629
16199
|
addCommonOptions(listCmd);
|
|
15630
|
-
listCmd.action(async (options2) => {
|
|
16200
|
+
listCmd.action(async (options2, command) => {
|
|
15631
16201
|
try {
|
|
15632
|
-
const
|
|
15633
|
-
|
|
15634
|
-
|
|
15635
|
-
|
|
15636
|
-
return;
|
|
16202
|
+
const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
|
|
16203
|
+
if (options2.global && cwdExplicitlyProvided) {
|
|
16204
|
+
logger.error("Cannot use --global with --cwd. They are mutually exclusive.");
|
|
16205
|
+
process.exit(1);
|
|
15637
16206
|
}
|
|
16207
|
+
let configDir;
|
|
16208
|
+
const config = await (async () => {
|
|
16209
|
+
if (options2.global) {
|
|
16210
|
+
configDir = getGlobalConfigPath();
|
|
16211
|
+
const cfg = await readOcxConfig(configDir);
|
|
16212
|
+
if (!cfg) {
|
|
16213
|
+
logger.warn("Global config not found. Run 'opencode' once to initialize.");
|
|
16214
|
+
return null;
|
|
16215
|
+
}
|
|
16216
|
+
return cfg;
|
|
16217
|
+
} else {
|
|
16218
|
+
configDir = options2.cwd ?? process.cwd();
|
|
16219
|
+
const cfg = await readOcxConfig(configDir);
|
|
16220
|
+
if (!cfg) {
|
|
16221
|
+
logger.warn("No ocx.jsonc found. Run 'ocx init' first.");
|
|
16222
|
+
return null;
|
|
16223
|
+
}
|
|
16224
|
+
return cfg;
|
|
16225
|
+
}
|
|
16226
|
+
})();
|
|
16227
|
+
if (!config)
|
|
16228
|
+
return;
|
|
15638
16229
|
const result = runRegistryListCore({
|
|
15639
16230
|
getRegistries: () => config.registries,
|
|
15640
16231
|
isLocked: () => config.lockRegistries ?? false
|
|
@@ -15645,7 +16236,7 @@ function registerRegistryCommand(program2) {
|
|
|
15645
16236
|
if (result.registries.length === 0) {
|
|
15646
16237
|
logger.info("No registries configured.");
|
|
15647
16238
|
} else {
|
|
15648
|
-
logger.info(`Configured registries${result.locked ? kleur_default.yellow(" (locked)") : ""}:`);
|
|
16239
|
+
logger.info(`Configured registries${options2.global ? " (global)" : ""}${result.locked ? kleur_default.yellow(" (locked)") : ""}:`);
|
|
15649
16240
|
for (const reg of result.registries) {
|
|
15650
16241
|
console.log(` ${kleur_default.cyan(reg.name)}: ${reg.url} ${kleur_default.dim(`(${reg.version})`)}`);
|
|
15651
16242
|
}
|
|
@@ -15780,7 +16371,7 @@ async function runSearchCore(query, options2, provider) {
|
|
|
15780
16371
|
|
|
15781
16372
|
// src/self-update/version-provider.ts
|
|
15782
16373
|
class BuildTimeVersionProvider {
|
|
15783
|
-
version = "1.4.
|
|
16374
|
+
version = "1.4.1";
|
|
15784
16375
|
}
|
|
15785
16376
|
var defaultVersionProvider = new BuildTimeVersionProvider;
|
|
15786
16377
|
|
|
@@ -15893,7 +16484,7 @@ function getExecutablePath() {
|
|
|
15893
16484
|
}
|
|
15894
16485
|
|
|
15895
16486
|
// src/self-update/download.ts
|
|
15896
|
-
import { chmodSync, existsSync as
|
|
16487
|
+
import { chmodSync, existsSync as existsSync10, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
15897
16488
|
var GITHUB_REPO2 = "kdcokenny/ocx";
|
|
15898
16489
|
var DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${GITHUB_REPO2}/releases/download`;
|
|
15899
16490
|
var PLATFORM_MAP = {
|
|
@@ -15971,7 +16562,7 @@ async function downloadToTemp(version) {
|
|
|
15971
16562
|
try {
|
|
15972
16563
|
chmodSync(tempPath, 493);
|
|
15973
16564
|
} catch (error) {
|
|
15974
|
-
if (
|
|
16565
|
+
if (existsSync10(tempPath)) {
|
|
15975
16566
|
unlinkSync2(tempPath);
|
|
15976
16567
|
}
|
|
15977
16568
|
throw new SelfUpdateError(`Failed to set permissions: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -15981,20 +16572,20 @@ async function downloadToTemp(version) {
|
|
|
15981
16572
|
function atomicReplace(tempPath, execPath) {
|
|
15982
16573
|
const backupPath = `${execPath}.backup`;
|
|
15983
16574
|
try {
|
|
15984
|
-
if (
|
|
16575
|
+
if (existsSync10(execPath)) {
|
|
15985
16576
|
renameSync2(execPath, backupPath);
|
|
15986
16577
|
}
|
|
15987
16578
|
renameSync2(tempPath, execPath);
|
|
15988
|
-
if (
|
|
16579
|
+
if (existsSync10(backupPath)) {
|
|
15989
16580
|
unlinkSync2(backupPath);
|
|
15990
16581
|
}
|
|
15991
16582
|
} catch (error) {
|
|
15992
|
-
if (
|
|
16583
|
+
if (existsSync10(backupPath) && !existsSync10(execPath)) {
|
|
15993
16584
|
try {
|
|
15994
16585
|
renameSync2(backupPath, execPath);
|
|
15995
16586
|
} catch {}
|
|
15996
16587
|
}
|
|
15997
|
-
if (
|
|
16588
|
+
if (existsSync10(tempPath)) {
|
|
15998
16589
|
try {
|
|
15999
16590
|
unlinkSync2(tempPath);
|
|
16000
16591
|
} catch {}
|
|
@@ -16003,7 +16594,7 @@ function atomicReplace(tempPath, execPath) {
|
|
|
16003
16594
|
}
|
|
16004
16595
|
}
|
|
16005
16596
|
function cleanupTempFile(tempPath) {
|
|
16006
|
-
if (
|
|
16597
|
+
if (existsSync10(tempPath)) {
|
|
16007
16598
|
try {
|
|
16008
16599
|
unlinkSync2(tempPath);
|
|
16009
16600
|
} catch {}
|
|
@@ -16025,7 +16616,7 @@ function notifyUpdated(from, to) {
|
|
|
16025
16616
|
}
|
|
16026
16617
|
|
|
16027
16618
|
// src/self-update/verify.ts
|
|
16028
|
-
import { createHash as
|
|
16619
|
+
import { createHash as createHash3 } from "crypto";
|
|
16029
16620
|
var GITHUB_REPO3 = "kdcokenny/ocx";
|
|
16030
16621
|
function parseSha256Sums(content2) {
|
|
16031
16622
|
const checksums = new Map;
|
|
@@ -16038,8 +16629,8 @@ function parseSha256Sums(content2) {
|
|
|
16038
16629
|
}
|
|
16039
16630
|
return checksums;
|
|
16040
16631
|
}
|
|
16041
|
-
function
|
|
16042
|
-
return
|
|
16632
|
+
function hashContent3(content2) {
|
|
16633
|
+
return createHash3("sha256").update(content2).digest("hex");
|
|
16043
16634
|
}
|
|
16044
16635
|
async function fetchChecksums(version) {
|
|
16045
16636
|
const url = `https://github.com/${GITHUB_REPO3}/releases/download/v${version}/SHA256SUMS.txt`;
|
|
@@ -16053,7 +16644,7 @@ async function fetchChecksums(version) {
|
|
|
16053
16644
|
async function verifyChecksum(filePath, expectedHash, filename) {
|
|
16054
16645
|
const file = Bun.file(filePath);
|
|
16055
16646
|
const content2 = await file.arrayBuffer();
|
|
16056
|
-
const actualHash =
|
|
16647
|
+
const actualHash = hashContent3(Buffer.from(content2));
|
|
16057
16648
|
if (actualHash !== expectedHash) {
|
|
16058
16649
|
throw new IntegrityError(filename, expectedHash, actualHash);
|
|
16059
16650
|
}
|
|
@@ -16178,10 +16769,10 @@ function registerSelfCommand(program2) {
|
|
|
16178
16769
|
}
|
|
16179
16770
|
|
|
16180
16771
|
// src/commands/update.ts
|
|
16181
|
-
import { createHash as
|
|
16182
|
-
import { existsSync as
|
|
16183
|
-
import { mkdir as
|
|
16184
|
-
import { dirname as
|
|
16772
|
+
import { createHash as createHash4 } from "crypto";
|
|
16773
|
+
import { existsSync as existsSync11 } from "fs";
|
|
16774
|
+
import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
|
|
16775
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
16185
16776
|
function registerUpdateCommand(program2) {
|
|
16186
16777
|
program2.command("update [components...]").description("Update installed components (use @version suffix to pin, e.g., kdco/agents@1.2.0)").option("--all", "Update all installed components").option("--registry <name>", "Update all components from a specific registry").option("--dry-run", "Preview changes without applying").option("--cwd <path>", "Working directory", process.cwd()).option("-q, --quiet", "Suppress output").option("-v, --verbose", "Verbose output").option("--json", "Output as JSON").action(async (components, options2) => {
|
|
16187
16778
|
try {
|
|
@@ -16265,7 +16856,7 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
|
|
|
16265
16856
|
const content2 = await fetchFileContent(regConfig.url, componentName, file.path);
|
|
16266
16857
|
files.push({ path: file.path, content: Buffer.from(content2) });
|
|
16267
16858
|
}
|
|
16268
|
-
const newHash = await
|
|
16859
|
+
const newHash = await hashBundle3(files);
|
|
16269
16860
|
if (newHash === lockEntry.hash) {
|
|
16270
16861
|
results.push({
|
|
16271
16862
|
qualifiedName,
|
|
@@ -16319,12 +16910,12 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
|
|
|
16319
16910
|
const fileObj = update.component.files.find((f) => f.path === file.path);
|
|
16320
16911
|
if (!fileObj)
|
|
16321
16912
|
continue;
|
|
16322
|
-
const targetPath =
|
|
16323
|
-
const targetDir =
|
|
16324
|
-
if (!
|
|
16325
|
-
await
|
|
16913
|
+
const targetPath = join9(provider.cwd, fileObj.target);
|
|
16914
|
+
const targetDir = dirname5(targetPath);
|
|
16915
|
+
if (!existsSync11(targetDir)) {
|
|
16916
|
+
await mkdir9(targetDir, { recursive: true });
|
|
16326
16917
|
}
|
|
16327
|
-
await
|
|
16918
|
+
await writeFile4(targetPath, file.content);
|
|
16328
16919
|
if (options2.verbose) {
|
|
16329
16920
|
logger.info(` \u2713 Updated ${fileObj.target}`);
|
|
16330
16921
|
}
|
|
@@ -16443,17 +17034,17 @@ function outputDryRun(results, options2) {
|
|
|
16443
17034
|
}
|
|
16444
17035
|
}
|
|
16445
17036
|
}
|
|
16446
|
-
async function
|
|
16447
|
-
return
|
|
17037
|
+
async function hashContent4(content2) {
|
|
17038
|
+
return createHash4("sha256").update(content2).digest("hex");
|
|
16448
17039
|
}
|
|
16449
|
-
async function
|
|
17040
|
+
async function hashBundle3(files) {
|
|
16450
17041
|
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
16451
17042
|
const manifestParts = [];
|
|
16452
17043
|
for (const file of sorted) {
|
|
16453
|
-
const hash = await
|
|
17044
|
+
const hash = await hashContent4(file.content);
|
|
16454
17045
|
manifestParts.push(`${file.path}:${hash}`);
|
|
16455
17046
|
}
|
|
16456
|
-
return
|
|
17047
|
+
return hashContent4(manifestParts.join(`
|
|
16457
17048
|
`));
|
|
16458
17049
|
}
|
|
16459
17050
|
|
|
@@ -16485,7 +17076,7 @@ function registerUpdateCheckHook(program2) {
|
|
|
16485
17076
|
});
|
|
16486
17077
|
}
|
|
16487
17078
|
// src/index.ts
|
|
16488
|
-
var version = "1.4.
|
|
17079
|
+
var version = "1.4.1";
|
|
16489
17080
|
async function main2() {
|
|
16490
17081
|
const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
|
|
16491
17082
|
registerInitCommand(program2);
|
|
@@ -16517,4 +17108,4 @@ export {
|
|
|
16517
17108
|
buildRegistry
|
|
16518
17109
|
};
|
|
16519
17110
|
|
|
16520
|
-
//# debugId=
|
|
17111
|
+
//# debugId=4E10E39074E11B3A64756E2164756E21
|