ocx 1.4.0 → 1.4.2
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 +1019 -280
- package/dist/index.js.map +24 -24
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9814,6 +9814,142 @@ 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
|
+
CONFLICT: 6
|
|
9829
|
+
};
|
|
9830
|
+
|
|
9831
|
+
class OCXError extends Error {
|
|
9832
|
+
code;
|
|
9833
|
+
exitCode;
|
|
9834
|
+
constructor(message, code, exitCode = EXIT_CODES.GENERAL) {
|
|
9835
|
+
super(message);
|
|
9836
|
+
this.code = code;
|
|
9837
|
+
this.exitCode = exitCode;
|
|
9838
|
+
this.name = "OCXError";
|
|
9839
|
+
}
|
|
9840
|
+
}
|
|
9841
|
+
|
|
9842
|
+
class NotFoundError extends OCXError {
|
|
9843
|
+
constructor(message) {
|
|
9844
|
+
super(message, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
9845
|
+
this.name = "NotFoundError";
|
|
9846
|
+
}
|
|
9847
|
+
}
|
|
9848
|
+
|
|
9849
|
+
class NetworkError extends OCXError {
|
|
9850
|
+
constructor(message) {
|
|
9851
|
+
super(message, "NETWORK_ERROR", EXIT_CODES.NETWORK);
|
|
9852
|
+
this.name = "NetworkError";
|
|
9853
|
+
}
|
|
9854
|
+
}
|
|
9855
|
+
|
|
9856
|
+
class ConfigError extends OCXError {
|
|
9857
|
+
constructor(message) {
|
|
9858
|
+
super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
9859
|
+
this.name = "ConfigError";
|
|
9860
|
+
}
|
|
9861
|
+
}
|
|
9862
|
+
|
|
9863
|
+
class ValidationError extends OCXError {
|
|
9864
|
+
constructor(message) {
|
|
9865
|
+
super(message, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
|
|
9866
|
+
this.name = "ValidationError";
|
|
9867
|
+
}
|
|
9868
|
+
}
|
|
9869
|
+
|
|
9870
|
+
class ConflictError extends OCXError {
|
|
9871
|
+
constructor(message) {
|
|
9872
|
+
super(message, "CONFLICT", EXIT_CODES.CONFLICT);
|
|
9873
|
+
this.name = "ConflictError";
|
|
9874
|
+
}
|
|
9875
|
+
}
|
|
9876
|
+
|
|
9877
|
+
class IntegrityError extends OCXError {
|
|
9878
|
+
constructor(component, expected, found) {
|
|
9879
|
+
const message = `Integrity verification failed for "${component}"
|
|
9880
|
+
` + ` Expected: ${expected}
|
|
9881
|
+
` + ` Found: ${found}
|
|
9882
|
+
|
|
9883
|
+
` + `The registry content has changed since this component was locked.
|
|
9884
|
+
` + `Use 'ocx update ${component}' to intentionally update this component.`;
|
|
9885
|
+
super(message, "INTEGRITY_ERROR", EXIT_CODES.INTEGRITY);
|
|
9886
|
+
this.name = "IntegrityError";
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
|
|
9890
|
+
class SelfUpdateError extends OCXError {
|
|
9891
|
+
constructor(message) {
|
|
9892
|
+
super(message, "UPDATE_ERROR", EXIT_CODES.GENERAL);
|
|
9893
|
+
this.name = "SelfUpdateError";
|
|
9894
|
+
}
|
|
9895
|
+
}
|
|
9896
|
+
|
|
9897
|
+
class OcxConfigError extends OCXError {
|
|
9898
|
+
constructor(message) {
|
|
9899
|
+
super(message, "CONFIG_ERROR", EXIT_CODES.CONFIG);
|
|
9900
|
+
this.name = "OcxConfigError";
|
|
9901
|
+
}
|
|
9902
|
+
}
|
|
9903
|
+
|
|
9904
|
+
class ProfileNotFoundError extends OCXError {
|
|
9905
|
+
constructor(name) {
|
|
9906
|
+
super(`Profile "${name}" not found`, "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
9907
|
+
this.name = "ProfileNotFoundError";
|
|
9908
|
+
}
|
|
9909
|
+
}
|
|
9910
|
+
|
|
9911
|
+
class ProfileExistsError extends OCXError {
|
|
9912
|
+
constructor(name) {
|
|
9913
|
+
super(`Profile "${name}" already exists. Use --force to overwrite.`, "CONFLICT", EXIT_CODES.CONFLICT);
|
|
9914
|
+
this.name = "ProfileExistsError";
|
|
9915
|
+
}
|
|
9916
|
+
}
|
|
9917
|
+
|
|
9918
|
+
class RegistryExistsError extends OCXError {
|
|
9919
|
+
registryName;
|
|
9920
|
+
existingUrl;
|
|
9921
|
+
newUrl;
|
|
9922
|
+
targetLabel;
|
|
9923
|
+
constructor(registryName, existingUrl, newUrl, targetLabel) {
|
|
9924
|
+
const target = targetLabel ? ` in ${targetLabel}` : "";
|
|
9925
|
+
const message = `Registry "${registryName}" already exists${target}.
|
|
9926
|
+
` + ` Current: ${existingUrl}
|
|
9927
|
+
` + ` New: ${newUrl}
|
|
9928
|
+
|
|
9929
|
+
` + `Use --force to overwrite.`;
|
|
9930
|
+
super(message, "CONFLICT", EXIT_CODES.CONFLICT);
|
|
9931
|
+
this.registryName = registryName;
|
|
9932
|
+
this.existingUrl = existingUrl;
|
|
9933
|
+
this.newUrl = newUrl;
|
|
9934
|
+
this.targetLabel = targetLabel;
|
|
9935
|
+
this.name = "RegistryExistsError";
|
|
9936
|
+
}
|
|
9937
|
+
}
|
|
9938
|
+
|
|
9939
|
+
class InvalidProfileNameError extends OCXError {
|
|
9940
|
+
constructor(name, reason) {
|
|
9941
|
+
super(`Invalid profile name "${name}": ${reason}`, "VALIDATION_ERROR", EXIT_CODES.GENERAL);
|
|
9942
|
+
this.name = "InvalidProfileNameError";
|
|
9943
|
+
}
|
|
9944
|
+
}
|
|
9945
|
+
|
|
9946
|
+
class ProfilesNotInitializedError extends OCXError {
|
|
9947
|
+
constructor() {
|
|
9948
|
+
super("Profiles not initialized. Run 'ocx init --global' first.", "NOT_FOUND", EXIT_CODES.NOT_FOUND);
|
|
9949
|
+
this.name = "ProfilesNotInitializedError";
|
|
9950
|
+
}
|
|
9951
|
+
}
|
|
9952
|
+
|
|
9817
9953
|
// src/schemas/registry.ts
|
|
9818
9954
|
var npmSpecifierSchema = exports_external.string().refine((val) => val.startsWith("npm:"), {
|
|
9819
9955
|
message: 'npm specifier must start with "npm:" prefix'
|
|
@@ -9860,8 +9996,10 @@ var componentTypeSchema = exports_external.enum([
|
|
|
9860
9996
|
"ocx:plugin",
|
|
9861
9997
|
"ocx:command",
|
|
9862
9998
|
"ocx:tool",
|
|
9863
|
-
"ocx:bundle"
|
|
9999
|
+
"ocx:bundle",
|
|
10000
|
+
"ocx:profile"
|
|
9864
10001
|
]);
|
|
10002
|
+
var profileTargetPathSchema = exports_external.enum(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
|
|
9865
10003
|
var targetPathSchema = exports_external.string().refine((path) => path.startsWith(".opencode/"), {
|
|
9866
10004
|
message: 'Target path must start with ".opencode/"'
|
|
9867
10005
|
}).refine((path) => {
|
|
@@ -9901,7 +10039,7 @@ var mcpServerObjectSchema = exports_external.object({
|
|
|
9901
10039
|
var mcpServerRefSchema = exports_external.union([exports_external.string(), mcpServerObjectSchema]);
|
|
9902
10040
|
var componentFileObjectSchema = exports_external.object({
|
|
9903
10041
|
path: exports_external.string().min(1, "File path cannot be empty"),
|
|
9904
|
-
target:
|
|
10042
|
+
target: exports_external.string().min(1, "Target path cannot be empty")
|
|
9905
10043
|
});
|
|
9906
10044
|
var componentFileSchema = exports_external.union([
|
|
9907
10045
|
exports_external.string().min(1, "File path cannot be empty"),
|
|
@@ -9993,16 +10131,56 @@ var componentManifestSchema = exports_external.object({
|
|
|
9993
10131
|
npmDevDependencies: exports_external.array(exports_external.string()).optional(),
|
|
9994
10132
|
opencode: opencodeConfigSchema.optional()
|
|
9995
10133
|
});
|
|
10134
|
+
function validateSafePath(filePath) {
|
|
10135
|
+
if (isAbsolute(filePath)) {
|
|
10136
|
+
throw new ValidationError(`Invalid path: "${filePath}" - absolute paths not allowed`);
|
|
10137
|
+
}
|
|
10138
|
+
if (filePath.startsWith("~")) {
|
|
10139
|
+
throw new ValidationError(`Invalid path: "${filePath}" - home directory paths not allowed`);
|
|
10140
|
+
}
|
|
10141
|
+
const normalized = normalize(filePath);
|
|
10142
|
+
if (normalized.startsWith("..")) {
|
|
10143
|
+
throw new ValidationError(`Invalid path: "${filePath}" - path traversal not allowed`);
|
|
10144
|
+
}
|
|
10145
|
+
}
|
|
9996
10146
|
function inferTargetPath(sourcePath) {
|
|
9997
10147
|
return `.opencode/${sourcePath}`;
|
|
9998
10148
|
}
|
|
9999
|
-
function
|
|
10149
|
+
function validateFileTarget(target, componentType) {
|
|
10150
|
+
const isProfile = componentType === "ocx:profile";
|
|
10151
|
+
if (isProfile) {
|
|
10152
|
+
const isProfileFile = profileTargetPathSchema.safeParse(target).success;
|
|
10153
|
+
const isOpencodeTarget = target.startsWith(".opencode/");
|
|
10154
|
+
if (!isProfileFile && !isOpencodeTarget) {
|
|
10155
|
+
throw new ValidationError(`Invalid profile target: "${target}". ` + `Must be a profile file (ocx.jsonc, opencode.jsonc, AGENTS.md) or start with ".opencode/"`);
|
|
10156
|
+
}
|
|
10157
|
+
if (isOpencodeTarget) {
|
|
10158
|
+
const parseResult = targetPathSchema.safeParse(target);
|
|
10159
|
+
if (!parseResult.success) {
|
|
10160
|
+
throw new ValidationError(`Invalid embedded target: "${target}". ${parseResult.error.errors[0]?.message}`);
|
|
10161
|
+
}
|
|
10162
|
+
}
|
|
10163
|
+
} else {
|
|
10164
|
+
const parseResult = targetPathSchema.safeParse(target);
|
|
10165
|
+
if (!parseResult.success) {
|
|
10166
|
+
throw new ValidationError(`Invalid target: "${target}". ${parseResult.error.errors[0]?.message}`);
|
|
10167
|
+
}
|
|
10168
|
+
}
|
|
10169
|
+
}
|
|
10170
|
+
function normalizeFile(file, componentType) {
|
|
10171
|
+
const isProfile = componentType === "ocx:profile";
|
|
10000
10172
|
if (typeof file === "string") {
|
|
10173
|
+
validateSafePath(file);
|
|
10174
|
+
const target = isProfile ? file : inferTargetPath(file);
|
|
10175
|
+
validateFileTarget(target, componentType);
|
|
10001
10176
|
return {
|
|
10002
10177
|
path: file,
|
|
10003
|
-
target
|
|
10178
|
+
target
|
|
10004
10179
|
};
|
|
10005
10180
|
}
|
|
10181
|
+
validateSafePath(file.path);
|
|
10182
|
+
validateSafePath(file.target);
|
|
10183
|
+
validateFileTarget(file.target, componentType);
|
|
10006
10184
|
return file;
|
|
10007
10185
|
}
|
|
10008
10186
|
function normalizeMcpServer(server) {
|
|
@@ -10028,7 +10206,7 @@ function normalizeComponentManifest(manifest) {
|
|
|
10028
10206
|
}
|
|
10029
10207
|
return {
|
|
10030
10208
|
...manifest,
|
|
10031
|
-
files: manifest.files.map(normalizeFile),
|
|
10209
|
+
files: manifest.files.map((file) => normalizeFile(file, manifest.type)),
|
|
10032
10210
|
opencode: normalizedOpencode
|
|
10033
10211
|
};
|
|
10034
10212
|
}
|
|
@@ -10128,9 +10306,15 @@ function findOcxConfig(cwd) {
|
|
|
10128
10306
|
}
|
|
10129
10307
|
return { path: dotOpencodePath, exists: false };
|
|
10130
10308
|
}
|
|
10131
|
-
function findOcxLock(cwd) {
|
|
10309
|
+
function findOcxLock(cwd, options) {
|
|
10132
10310
|
const dotOpencodePath = path.join(cwd, LOCAL_CONFIG_DIR, LOCK_FILE);
|
|
10133
10311
|
const rootPath = path.join(cwd, LOCK_FILE);
|
|
10312
|
+
if (options?.isFlattened) {
|
|
10313
|
+
if (existsSync(rootPath)) {
|
|
10314
|
+
return { path: rootPath, exists: true };
|
|
10315
|
+
}
|
|
10316
|
+
return { path: rootPath, exists: false };
|
|
10317
|
+
}
|
|
10134
10318
|
if (existsSync(dotOpencodePath)) {
|
|
10135
10319
|
return { path: dotOpencodePath, exists: true };
|
|
10136
10320
|
}
|
|
@@ -10160,8 +10344,8 @@ async function writeOcxConfig(cwd, config, existingPath) {
|
|
|
10160
10344
|
const content = JSON.stringify(config, null, 2);
|
|
10161
10345
|
await Bun.write(configPath, content);
|
|
10162
10346
|
}
|
|
10163
|
-
async function readOcxLock(cwd) {
|
|
10164
|
-
const { path: lockPath, exists } = findOcxLock(cwd);
|
|
10347
|
+
async function readOcxLock(cwd, options) {
|
|
10348
|
+
const { path: lockPath, exists } = findOcxLock(cwd, options);
|
|
10165
10349
|
if (!exists) {
|
|
10166
10350
|
return null;
|
|
10167
10351
|
}
|
|
@@ -10177,124 +10361,13 @@ async function writeOcxLock(cwd, lock, existingPath) {
|
|
|
10177
10361
|
await Bun.write(lockPath, content);
|
|
10178
10362
|
}
|
|
10179
10363
|
|
|
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
10364
|
// src/utils/paths.ts
|
|
10292
10365
|
import { stat } from "fs/promises";
|
|
10293
10366
|
import { homedir } from "os";
|
|
10294
|
-
import { isAbsolute, join } from "path";
|
|
10367
|
+
import { isAbsolute as isAbsolute2, join } from "path";
|
|
10295
10368
|
function getGlobalConfigPath() {
|
|
10296
10369
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
10297
|
-
const base = xdg &&
|
|
10370
|
+
const base = xdg && isAbsolute2(xdg) ? xdg : join(homedir(), ".config");
|
|
10298
10371
|
return join(base, "opencode");
|
|
10299
10372
|
}
|
|
10300
10373
|
async function globalDirectoryExists() {
|
|
@@ -10305,8 +10378,8 @@ async function globalDirectoryExists() {
|
|
|
10305
10378
|
return false;
|
|
10306
10379
|
}
|
|
10307
10380
|
}
|
|
10308
|
-
function resolveTargetPath(target,
|
|
10309
|
-
if (
|
|
10381
|
+
function resolveTargetPath(target, isFlattened) {
|
|
10382
|
+
if (isFlattened && target.startsWith(".opencode/")) {
|
|
10310
10383
|
return target.slice(".opencode/".length);
|
|
10311
10384
|
}
|
|
10312
10385
|
return target;
|
|
@@ -10348,7 +10421,7 @@ class GlobalConfigProvider {
|
|
|
10348
10421
|
static async requireInitialized() {
|
|
10349
10422
|
const basePath = getGlobalConfigPath();
|
|
10350
10423
|
if (!await globalDirectoryExists()) {
|
|
10351
|
-
throw new ConfigError("Global config not found. Run '
|
|
10424
|
+
throw new ConfigError("Global config not found. Run 'ocx init --global' first.");
|
|
10352
10425
|
}
|
|
10353
10426
|
const config = await readOcxConfig(basePath);
|
|
10354
10427
|
return new GlobalConfigProvider(basePath, config);
|
|
@@ -10575,7 +10648,25 @@ class ProfileManager {
|
|
|
10575
10648
|
const dir = getProfileDir(name);
|
|
10576
10649
|
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
10577
10650
|
const ocxPath = getProfileOcxConfig(name);
|
|
10578
|
-
|
|
10651
|
+
const ocxFile = Bun.file(ocxPath);
|
|
10652
|
+
if (!await ocxFile.exists()) {
|
|
10653
|
+
await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
|
|
10654
|
+
}
|
|
10655
|
+
const opencodePath = getProfileOpencodeConfig(name);
|
|
10656
|
+
const opencodeFile = Bun.file(opencodePath);
|
|
10657
|
+
if (!await opencodeFile.exists()) {
|
|
10658
|
+
await atomicWrite(opencodePath, {});
|
|
10659
|
+
}
|
|
10660
|
+
const agentsPath = getProfileAgents(name);
|
|
10661
|
+
const agentsFile = Bun.file(agentsPath);
|
|
10662
|
+
if (!await agentsFile.exists()) {
|
|
10663
|
+
const agentsContent = `# Profile Instructions
|
|
10664
|
+
|
|
10665
|
+
<!-- Add your custom instructions for this profile here -->
|
|
10666
|
+
<!-- These will be included when running \`ocx opencode -p ${name}\` -->
|
|
10667
|
+
`;
|
|
10668
|
+
await Bun.write(agentsPath, agentsContent, { mode: 384 });
|
|
10669
|
+
}
|
|
10579
10670
|
}
|
|
10580
10671
|
async remove(name) {
|
|
10581
10672
|
if (!await this.exists(name)) {
|
|
@@ -10714,11 +10805,13 @@ class ConfigResolver {
|
|
|
10714
10805
|
}
|
|
10715
10806
|
}
|
|
10716
10807
|
const shouldLoadLocal = this.shouldLoadLocalConfig();
|
|
10717
|
-
if (shouldLoadLocal && this.localConfigDir) {
|
|
10808
|
+
if (!this.profile && shouldLoadLocal && this.localConfigDir) {
|
|
10718
10809
|
const localOcxConfig = this.loadLocalOcxConfig();
|
|
10719
10810
|
if (localOcxConfig) {
|
|
10720
|
-
registries =
|
|
10811
|
+
registries = localOcxConfig.registries;
|
|
10721
10812
|
}
|
|
10813
|
+
}
|
|
10814
|
+
if (shouldLoadLocal && this.localConfigDir) {
|
|
10722
10815
|
const localOpencodeConfig = this.loadLocalOpencodeConfig();
|
|
10723
10816
|
if (localOpencodeConfig) {
|
|
10724
10817
|
opencode = this.deepMerge(opencode, localOpencodeConfig);
|
|
@@ -10759,7 +10852,7 @@ class ConfigResolver {
|
|
|
10759
10852
|
}
|
|
10760
10853
|
}
|
|
10761
10854
|
const shouldLoadLocal = this.shouldLoadLocalConfig();
|
|
10762
|
-
if (shouldLoadLocal && this.localConfigDir) {
|
|
10855
|
+
if (!this.profile && shouldLoadLocal && this.localConfigDir) {
|
|
10763
10856
|
const localOcxConfig = this.loadLocalOcxConfig();
|
|
10764
10857
|
if (localOcxConfig) {
|
|
10765
10858
|
const localOcxPath = join2(this.localConfigDir, OCX_CONFIG_FILE);
|
|
@@ -10768,6 +10861,8 @@ class ConfigResolver {
|
|
|
10768
10861
|
origins.set(`registries.${key}`, { path: localOcxPath, source: "local-config" });
|
|
10769
10862
|
}
|
|
10770
10863
|
}
|
|
10864
|
+
}
|
|
10865
|
+
if (shouldLoadLocal && this.localConfigDir) {
|
|
10771
10866
|
const localOpencodeConfig = this.loadLocalOpencodeConfig();
|
|
10772
10867
|
if (localOpencodeConfig) {
|
|
10773
10868
|
opencode = this.deepMerge(opencode, localOpencodeConfig);
|
|
@@ -10887,7 +10982,7 @@ class ConfigResolver {
|
|
|
10887
10982
|
// package.json
|
|
10888
10983
|
var package_default = {
|
|
10889
10984
|
name: "ocx",
|
|
10890
|
-
version: "1.4.
|
|
10985
|
+
version: "1.4.2",
|
|
10891
10986
|
description: "OCX CLI - ShadCN-style registry for OpenCode extensions. Install agents, plugins, skills, and MCP servers.",
|
|
10892
10987
|
author: "kdcokenny",
|
|
10893
10988
|
license: "MIT",
|
|
@@ -11162,8 +11257,19 @@ async function resolveDependencies(registries, componentNames) {
|
|
|
11162
11257
|
// src/updaters/update-opencode-config.ts
|
|
11163
11258
|
import { existsSync as existsSync4 } from "fs";
|
|
11164
11259
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
11260
|
+
import { homedir as homedir3 } from "os";
|
|
11165
11261
|
import path4 from "path";
|
|
11166
11262
|
var LOCAL_CONFIG_DIR3 = ".opencode";
|
|
11263
|
+
function isGlobalConfigPath(cwd) {
|
|
11264
|
+
const base = process.env.XDG_CONFIG_HOME || path4.join(homedir3(), ".config");
|
|
11265
|
+
const globalConfigDir = path4.resolve(base, "opencode");
|
|
11266
|
+
const resolvedCwd = path4.resolve(cwd);
|
|
11267
|
+
if (resolvedCwd === globalConfigDir) {
|
|
11268
|
+
return true;
|
|
11269
|
+
}
|
|
11270
|
+
const relative2 = path4.relative(globalConfigDir, resolvedCwd);
|
|
11271
|
+
return relative2 !== "" && !relative2.startsWith("..") && !path4.isAbsolute(relative2);
|
|
11272
|
+
}
|
|
11167
11273
|
var JSONC_OPTIONS = {
|
|
11168
11274
|
formattingOptions: {
|
|
11169
11275
|
tabSize: 2,
|
|
@@ -11178,6 +11284,17 @@ var OPENCODE_CONFIG_TEMPLATE = `{
|
|
|
11178
11284
|
}
|
|
11179
11285
|
`;
|
|
11180
11286
|
function findOpencodeConfig(cwd) {
|
|
11287
|
+
if (isGlobalConfigPath(cwd)) {
|
|
11288
|
+
const rootJsonc2 = path4.join(cwd, "opencode.jsonc");
|
|
11289
|
+
const rootJson2 = path4.join(cwd, "opencode.json");
|
|
11290
|
+
if (existsSync4(rootJsonc2)) {
|
|
11291
|
+
return { path: rootJsonc2, exists: true };
|
|
11292
|
+
}
|
|
11293
|
+
if (existsSync4(rootJson2)) {
|
|
11294
|
+
return { path: rootJson2, exists: true };
|
|
11295
|
+
}
|
|
11296
|
+
return { path: rootJsonc2, exists: false };
|
|
11297
|
+
}
|
|
11181
11298
|
const dotOpencodeJsonc = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
|
|
11182
11299
|
const dotOpencodeJson = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.json");
|
|
11183
11300
|
if (existsSync4(dotOpencodeJsonc)) {
|
|
@@ -11267,7 +11384,7 @@ async function updateOpencodeJsonConfig(cwd, opencode) {
|
|
|
11267
11384
|
} else {
|
|
11268
11385
|
const config = { $schema: "https://opencode.ai/config.json" };
|
|
11269
11386
|
content = JSON.stringify(config, null, "\t");
|
|
11270
|
-
configPath = path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
|
|
11387
|
+
configPath = isGlobalConfigPath(cwd) ? path4.join(cwd, "opencode.jsonc") : path4.join(cwd, LOCAL_CONFIG_DIR3, "opencode.jsonc");
|
|
11271
11388
|
await mkdir3(path4.dirname(configPath), { recursive: true });
|
|
11272
11389
|
created = true;
|
|
11273
11390
|
}
|
|
@@ -11559,6 +11676,25 @@ function wrapAction(action) {
|
|
|
11559
11676
|
};
|
|
11560
11677
|
}
|
|
11561
11678
|
function formatErrorAsJson(error) {
|
|
11679
|
+
if (error instanceof RegistryExistsError) {
|
|
11680
|
+
return {
|
|
11681
|
+
success: false,
|
|
11682
|
+
error: {
|
|
11683
|
+
code: error.code,
|
|
11684
|
+
message: error.message,
|
|
11685
|
+
details: {
|
|
11686
|
+
registryName: error.registryName,
|
|
11687
|
+
existingUrl: error.existingUrl,
|
|
11688
|
+
newUrl: error.newUrl,
|
|
11689
|
+
...error.targetLabel && { targetLabel: error.targetLabel }
|
|
11690
|
+
}
|
|
11691
|
+
},
|
|
11692
|
+
exitCode: error.exitCode,
|
|
11693
|
+
meta: {
|
|
11694
|
+
timestamp: new Date().toISOString()
|
|
11695
|
+
}
|
|
11696
|
+
};
|
|
11697
|
+
}
|
|
11562
11698
|
if (error instanceof OCXError) {
|
|
11563
11699
|
return {
|
|
11564
11700
|
success: false,
|
|
@@ -11622,6 +11758,7 @@ var sharedOptions = {
|
|
|
11622
11758
|
cwd: () => new Option("--cwd <path>", "Working directory").default(process.cwd()),
|
|
11623
11759
|
quiet: () => new Option("-q, --quiet", "Suppress output"),
|
|
11624
11760
|
json: () => new Option("--json", "Output as JSON"),
|
|
11761
|
+
profile: () => new Option("-p, --profile <name>", "Target a specific profile's config"),
|
|
11625
11762
|
force: () => new Option("-f, --force", "Skip confirmation prompts"),
|
|
11626
11763
|
verbose: () => new Option("-v, --verbose", "Verbose output"),
|
|
11627
11764
|
global: new Option("-g, --global", "Install to global OpenCode config (~/.config/opencode)")
|
|
@@ -11638,6 +11775,20 @@ function addVerboseOption(cmd) {
|
|
|
11638
11775
|
function addGlobalOption(cmd) {
|
|
11639
11776
|
return cmd.addOption(sharedOptions.global);
|
|
11640
11777
|
}
|
|
11778
|
+
function addProfileOption(cmd) {
|
|
11779
|
+
return cmd.addOption(sharedOptions.profile());
|
|
11780
|
+
}
|
|
11781
|
+
function validateProfileName(name) {
|
|
11782
|
+
if (!name || name.length === 0) {
|
|
11783
|
+
throw new InvalidProfileNameError(name, "cannot be empty");
|
|
11784
|
+
}
|
|
11785
|
+
if (name.length > 32) {
|
|
11786
|
+
throw new InvalidProfileNameError(name, "must be 32 characters or less");
|
|
11787
|
+
}
|
|
11788
|
+
if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) {
|
|
11789
|
+
throw new InvalidProfileNameError(name, "must start with a letter and contain only alphanumeric characters, dots, underscores, or hyphens");
|
|
11790
|
+
}
|
|
11791
|
+
}
|
|
11641
11792
|
// ../../node_modules/.bun/ora@8.2.0/node_modules/ora/index.js
|
|
11642
11793
|
import process9 from "process";
|
|
11643
11794
|
|
|
@@ -13182,8 +13333,9 @@ function registerAddCommand(program2) {
|
|
|
13182
13333
|
const resolver = await ConfigResolver.create(options2.cwd ?? process.cwd(), {
|
|
13183
13334
|
profile: options2.profile
|
|
13184
13335
|
});
|
|
13336
|
+
const profileDir = getProfileDir(options2.profile);
|
|
13185
13337
|
provider = {
|
|
13186
|
-
cwd:
|
|
13338
|
+
cwd: profileDir,
|
|
13187
13339
|
getRegistries: () => resolver.getRegistries(),
|
|
13188
13340
|
getComponentPath: () => resolver.getComponentPath()
|
|
13189
13341
|
};
|
|
@@ -13298,10 +13450,11 @@ async function handleNpmPlugins(inputs, options2, cwd) {
|
|
|
13298
13450
|
}
|
|
13299
13451
|
async function runRegistryAddCore(componentNames, options2, provider) {
|
|
13300
13452
|
const cwd = provider.cwd;
|
|
13301
|
-
const
|
|
13453
|
+
const isFlattened = !!options2.global || !!options2.profile;
|
|
13454
|
+
const { path: lockPath } = findOcxLock(cwd, { isFlattened });
|
|
13302
13455
|
const registries = provider.getRegistries();
|
|
13303
13456
|
let lock = { lockVersion: 1, installed: {} };
|
|
13304
|
-
const existingLock = await readOcxLock(cwd);
|
|
13457
|
+
const existingLock = await readOcxLock(cwd, { isFlattened });
|
|
13305
13458
|
if (existingLock) {
|
|
13306
13459
|
lock = existingLock;
|
|
13307
13460
|
}
|
|
@@ -13354,7 +13507,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13354
13507
|
}
|
|
13355
13508
|
const computedHash = await hashBundle(files);
|
|
13356
13509
|
for (const file of component.files) {
|
|
13357
|
-
const targetPath = join3(cwd, resolveTargetPath(file.target,
|
|
13510
|
+
const targetPath = join3(cwd, resolveTargetPath(file.target, isFlattened));
|
|
13358
13511
|
assertPathInside(targetPath, cwd);
|
|
13359
13512
|
}
|
|
13360
13513
|
const existingEntry = lock.installed[component.qualifiedName];
|
|
@@ -13363,7 +13516,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13363
13516
|
throw new IntegrityError(component.qualifiedName, existingEntry.hash, computedHash);
|
|
13364
13517
|
}
|
|
13365
13518
|
for (const file of component.files) {
|
|
13366
|
-
const resolvedTarget = resolveTargetPath(file.target,
|
|
13519
|
+
const resolvedTarget = resolveTargetPath(file.target, isFlattened);
|
|
13367
13520
|
const targetPath = join3(cwd, resolvedTarget);
|
|
13368
13521
|
if (existsSync5(targetPath)) {
|
|
13369
13522
|
const conflictingComponent = findComponentByFile(lock, resolvedTarget);
|
|
@@ -13387,7 +13540,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13387
13540
|
const componentFile = component.files.find((f) => f.path === file.path);
|
|
13388
13541
|
if (!componentFile)
|
|
13389
13542
|
continue;
|
|
13390
|
-
const resolvedTarget = resolveTargetPath(componentFile.target,
|
|
13543
|
+
const resolvedTarget = resolveTargetPath(componentFile.target, isFlattened);
|
|
13391
13544
|
const targetPath = join3(cwd, resolvedTarget);
|
|
13392
13545
|
if (existsSync5(targetPath)) {
|
|
13393
13546
|
const existingContent = await Bun.file(targetPath).text();
|
|
@@ -13415,7 +13568,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13415
13568
|
const installResult = await installComponent(component, files, cwd, {
|
|
13416
13569
|
force: options2.force,
|
|
13417
13570
|
verbose: options2.verbose,
|
|
13418
|
-
|
|
13571
|
+
isFlattened
|
|
13419
13572
|
});
|
|
13420
13573
|
if (options2.verbose) {
|
|
13421
13574
|
for (const f of installResult.skipped) {
|
|
@@ -13436,7 +13589,7 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13436
13589
|
registry: component.registryName,
|
|
13437
13590
|
version: index.version,
|
|
13438
13591
|
hash: computedHash,
|
|
13439
|
-
files: component.files.map((f) => resolveTargetPath(f.target,
|
|
13592
|
+
files: component.files.map((f) => resolveTargetPath(f.target, isFlattened)),
|
|
13440
13593
|
installedAt: new Date().toISOString()
|
|
13441
13594
|
};
|
|
13442
13595
|
}
|
|
@@ -13453,12 +13606,12 @@ async function runRegistryAddCore(componentNames, options2, provider) {
|
|
|
13453
13606
|
}
|
|
13454
13607
|
const hasNpmDeps = resolved.npmDependencies.length > 0;
|
|
13455
13608
|
const hasNpmDevDeps = resolved.npmDevDependencies.length > 0;
|
|
13456
|
-
const packageJsonPath = options2.global ? join3(cwd, "package.json") : join3(cwd, ".opencode/package.json");
|
|
13609
|
+
const packageJsonPath = options2.global || options2.profile ? join3(cwd, "package.json") : join3(cwd, ".opencode/package.json");
|
|
13457
13610
|
if (hasNpmDeps || hasNpmDevDeps) {
|
|
13458
13611
|
const npmSpin = options2.quiet ? null : createSpinner({ text: `Updating ${packageJsonPath}...` });
|
|
13459
13612
|
npmSpin?.start();
|
|
13460
13613
|
try {
|
|
13461
|
-
await updateOpencodeDevDependencies(cwd, resolved.npmDependencies, resolved.npmDevDependencies, {
|
|
13614
|
+
await updateOpencodeDevDependencies(cwd, resolved.npmDependencies, resolved.npmDevDependencies, { isFlattened });
|
|
13462
13615
|
const totalDeps = resolved.npmDependencies.length + resolved.npmDevDependencies.length;
|
|
13463
13616
|
npmSpin?.succeed(`Added ${totalDeps} dependencies to ${packageJsonPath}`);
|
|
13464
13617
|
} catch (error) {
|
|
@@ -13494,7 +13647,7 @@ async function installComponent(component, files, cwd, options2) {
|
|
|
13494
13647
|
const componentFile = component.files.find((f) => f.path === file.path);
|
|
13495
13648
|
if (!componentFile)
|
|
13496
13649
|
continue;
|
|
13497
|
-
const resolvedTarget = resolveTargetPath(componentFile.target, !!options2.
|
|
13650
|
+
const resolvedTarget = resolveTargetPath(componentFile.target, !!options2.isFlattened);
|
|
13498
13651
|
const targetPath = join3(cwd, resolvedTarget);
|
|
13499
13652
|
const targetDir = dirname(targetPath);
|
|
13500
13653
|
if (existsSync5(targetPath)) {
|
|
@@ -13594,7 +13747,7 @@ async function readOpencodePackageJson(opencodeDir) {
|
|
|
13594
13747
|
return JSON.parse(content2);
|
|
13595
13748
|
} catch (e3) {
|
|
13596
13749
|
const message = e3 instanceof Error ? e3.message : String(e3);
|
|
13597
|
-
throw new ConfigError(`Invalid
|
|
13750
|
+
throw new ConfigError(`Invalid ${pkgPath}: ${message}`);
|
|
13598
13751
|
}
|
|
13599
13752
|
}
|
|
13600
13753
|
async function ensureManifestFilesAreTracked(opencodeDir) {
|
|
@@ -13621,14 +13774,14 @@ async function updateOpencodeDevDependencies(cwd, npmDeps, npmDevDeps, options2
|
|
|
13621
13774
|
const allDepSpecs = [...npmDeps, ...npmDevDeps];
|
|
13622
13775
|
if (allDepSpecs.length === 0)
|
|
13623
13776
|
return;
|
|
13624
|
-
const packageDir = options2.
|
|
13777
|
+
const packageDir = options2.isFlattened ? cwd : join3(cwd, ".opencode");
|
|
13625
13778
|
await mkdir4(packageDir, { recursive: true });
|
|
13626
13779
|
const parsedDeps = allDepSpecs.map(parseNpmDependency);
|
|
13627
13780
|
const existing = await readOpencodePackageJson(packageDir);
|
|
13628
13781
|
const updated = mergeDevDependencies(existing, parsedDeps);
|
|
13629
13782
|
await Bun.write(join3(packageDir, "package.json"), `${JSON.stringify(updated, null, 2)}
|
|
13630
13783
|
`);
|
|
13631
|
-
if (!options2.
|
|
13784
|
+
if (!options2.isFlattened) {
|
|
13632
13785
|
await ensureManifestFilesAreTracked(packageDir);
|
|
13633
13786
|
}
|
|
13634
13787
|
}
|
|
@@ -13689,7 +13842,7 @@ async function buildRegistry(options2) {
|
|
|
13689
13842
|
const packumentPath = join4(componentsDir, `${component.name}.json`);
|
|
13690
13843
|
await Bun.write(packumentPath, JSON.stringify(packument, null, 2));
|
|
13691
13844
|
for (const rawFile of component.files) {
|
|
13692
|
-
const file = normalizeFile(rawFile);
|
|
13845
|
+
const file = normalizeFile(rawFile, component.type);
|
|
13693
13846
|
const sourceFilePath = join4(sourcePath, "files", file.path);
|
|
13694
13847
|
const destFilePath = join4(componentsDir, component.name, file.path);
|
|
13695
13848
|
const destFileDir = dirname2(destFilePath);
|
|
@@ -13786,7 +13939,7 @@ import { existsSync as existsSync6 } from "fs";
|
|
|
13786
13939
|
import { mkdir as mkdir6 } from "fs/promises";
|
|
13787
13940
|
import { join as join6 } from "path";
|
|
13788
13941
|
function registerConfigEditCommand(parent) {
|
|
13789
|
-
parent.command("edit").description("Open configuration file in editor").option("-g, --global", "Edit global ocx.jsonc").action(async (options2) => {
|
|
13942
|
+
parent.command("edit").description("Open configuration file in editor").option("-g, --global", "Edit global ocx.jsonc").option("-p, --profile <name>", "Edit specific profile's config").action(async (options2) => {
|
|
13790
13943
|
try {
|
|
13791
13944
|
await runConfigEdit(options2);
|
|
13792
13945
|
} catch (error) {
|
|
@@ -13795,12 +13948,26 @@ function registerConfigEditCommand(parent) {
|
|
|
13795
13948
|
});
|
|
13796
13949
|
}
|
|
13797
13950
|
async function runConfigEdit(options2) {
|
|
13951
|
+
if (options2.global && options2.profile) {
|
|
13952
|
+
throw new ValidationError("Cannot use both --global and --profile flags");
|
|
13953
|
+
}
|
|
13798
13954
|
let configPath;
|
|
13799
|
-
if (options2.
|
|
13955
|
+
if (options2.profile) {
|
|
13956
|
+
const parseResult = profileNameSchema.safeParse(options2.profile);
|
|
13957
|
+
if (!parseResult.success) {
|
|
13958
|
+
throw new ValidationError(`Invalid profile name "${options2.profile}": ${parseResult.error.errors[0]?.message ?? "Invalid name"}`);
|
|
13959
|
+
}
|
|
13960
|
+
await ProfileManager.requireInitialized();
|
|
13961
|
+
const manager = ProfileManager.create();
|
|
13962
|
+
if (!await manager.exists(options2.profile)) {
|
|
13963
|
+
throw new ProfileNotFoundError(options2.profile);
|
|
13964
|
+
}
|
|
13965
|
+
configPath = getProfileOcxConfig(options2.profile);
|
|
13966
|
+
} else if (options2.global) {
|
|
13800
13967
|
configPath = getGlobalConfig();
|
|
13801
13968
|
if (!existsSync6(configPath)) {
|
|
13802
13969
|
throw new ConfigError(`Global config not found at ${configPath}.
|
|
13803
|
-
|
|
13970
|
+
Run 'ocx init --global' first.`);
|
|
13804
13971
|
}
|
|
13805
13972
|
} else {
|
|
13806
13973
|
const localConfigDir = findLocalConfigDir(process.cwd());
|
|
@@ -14851,11 +15018,40 @@ Diff for ${res.name}:`));
|
|
|
14851
15018
|
}
|
|
14852
15019
|
|
|
14853
15020
|
// src/commands/ghost/migrate.ts
|
|
14854
|
-
import {
|
|
15021
|
+
import {
|
|
15022
|
+
copyFileSync,
|
|
15023
|
+
cpSync,
|
|
15024
|
+
existsSync as existsSync7,
|
|
15025
|
+
lstatSync,
|
|
15026
|
+
mkdirSync,
|
|
15027
|
+
readdirSync,
|
|
15028
|
+
renameSync,
|
|
15029
|
+
rmdirSync,
|
|
15030
|
+
rmSync,
|
|
15031
|
+
unlinkSync
|
|
15032
|
+
} from "fs";
|
|
14855
15033
|
import path6 from "path";
|
|
14856
15034
|
var GHOST_CONFIG_FILE = "ghost.jsonc";
|
|
14857
15035
|
var BACKUP_EXT = ".bak";
|
|
14858
15036
|
var CURRENT_SYMLINK = "current";
|
|
15037
|
+
var FLATTEN_DIRS = ["plugin", "agent", "skill", "command"];
|
|
15038
|
+
function moveAtomically(source, destination, isDir) {
|
|
15039
|
+
try {
|
|
15040
|
+
renameSync(source, destination);
|
|
15041
|
+
} catch (err) {
|
|
15042
|
+
if (err instanceof Error && "code" in err && err.code === "EXDEV") {
|
|
15043
|
+
if (isDir) {
|
|
15044
|
+
cpSync(source, destination, { recursive: true });
|
|
15045
|
+
rmSync(source, { recursive: true, force: true });
|
|
15046
|
+
} else {
|
|
15047
|
+
copyFileSync(source, destination);
|
|
15048
|
+
unlinkSync(source);
|
|
15049
|
+
}
|
|
15050
|
+
} else {
|
|
15051
|
+
throw err;
|
|
15052
|
+
}
|
|
15053
|
+
}
|
|
15054
|
+
}
|
|
14859
15055
|
function planMigration() {
|
|
14860
15056
|
const profilesDir = getProfilesDir();
|
|
14861
15057
|
const plan = {
|
|
@@ -14899,6 +15095,55 @@ function planMigration() {
|
|
|
14899
15095
|
backup: backupPath
|
|
14900
15096
|
});
|
|
14901
15097
|
}
|
|
15098
|
+
for (const entry of entries) {
|
|
15099
|
+
if (!entry.isDirectory() || entry.name === CURRENT_SYMLINK)
|
|
15100
|
+
continue;
|
|
15101
|
+
const profileName = entry.name;
|
|
15102
|
+
const profileDir = path6.join(profilesDir, profileName);
|
|
15103
|
+
const dotOpencode = path6.join(profileDir, ".opencode");
|
|
15104
|
+
if (!existsSync7(dotOpencode))
|
|
15105
|
+
continue;
|
|
15106
|
+
for (const dir of FLATTEN_DIRS) {
|
|
15107
|
+
const source = path6.join(dotOpencode, dir);
|
|
15108
|
+
const destination = path6.join(profileDir, dir);
|
|
15109
|
+
if (!existsSync7(source))
|
|
15110
|
+
continue;
|
|
15111
|
+
try {
|
|
15112
|
+
const stat3 = lstatSync(source);
|
|
15113
|
+
if (!stat3.isDirectory()) {
|
|
15114
|
+
plan.skipped.push({
|
|
15115
|
+
profileName,
|
|
15116
|
+
reason: `.opencode/${dir} is not a directory`
|
|
15117
|
+
});
|
|
15118
|
+
continue;
|
|
15119
|
+
}
|
|
15120
|
+
} catch {
|
|
15121
|
+
continue;
|
|
15122
|
+
}
|
|
15123
|
+
if (existsSync7(destination)) {
|
|
15124
|
+
plan.skipped.push({
|
|
15125
|
+
profileName,
|
|
15126
|
+
reason: `${dir}/ exists in both .opencode/ and profile root`
|
|
15127
|
+
});
|
|
15128
|
+
continue;
|
|
15129
|
+
}
|
|
15130
|
+
const backupPath = source + BACKUP_EXT;
|
|
15131
|
+
if (existsSync7(backupPath)) {
|
|
15132
|
+
plan.skipped.push({
|
|
15133
|
+
profileName,
|
|
15134
|
+
reason: `backup already exists: .opencode/${dir}${BACKUP_EXT}`
|
|
15135
|
+
});
|
|
15136
|
+
continue;
|
|
15137
|
+
}
|
|
15138
|
+
plan.profiles.push({
|
|
15139
|
+
type: "move-dir",
|
|
15140
|
+
profileName,
|
|
15141
|
+
source,
|
|
15142
|
+
destination,
|
|
15143
|
+
backup: backupPath
|
|
15144
|
+
});
|
|
15145
|
+
}
|
|
15146
|
+
}
|
|
14902
15147
|
const currentPath = path6.join(profilesDir, CURRENT_SYMLINK);
|
|
14903
15148
|
if (existsSync7(currentPath)) {
|
|
14904
15149
|
try {
|
|
@@ -14924,15 +15169,20 @@ function executeMigration(plan) {
|
|
|
14924
15169
|
const completedRenames = [];
|
|
14925
15170
|
try {
|
|
14926
15171
|
for (const action of plan.profiles) {
|
|
14927
|
-
|
|
14928
|
-
|
|
15172
|
+
if (action.type === "rename") {
|
|
15173
|
+
copyFileSync(action.source, action.backup);
|
|
15174
|
+
} else {
|
|
15175
|
+
cpSync(action.source, action.backup, { recursive: true });
|
|
15176
|
+
}
|
|
15177
|
+
completedBackups.push({ path: action.backup, isDir: action.type === "move-dir" });
|
|
14929
15178
|
}
|
|
14930
15179
|
for (const action of plan.profiles) {
|
|
14931
|
-
|
|
15180
|
+
moveAtomically(action.source, action.destination, action.type === "move-dir");
|
|
14932
15181
|
completedRenames.push({
|
|
14933
15182
|
source: action.source,
|
|
14934
15183
|
destination: action.destination,
|
|
14935
|
-
backup: action.backup
|
|
15184
|
+
backup: action.backup,
|
|
15185
|
+
isDir: action.type === "move-dir"
|
|
14936
15186
|
});
|
|
14937
15187
|
}
|
|
14938
15188
|
if (plan.symlink) {
|
|
@@ -14945,32 +15195,61 @@ function executeMigration(plan) {
|
|
|
14945
15195
|
}
|
|
14946
15196
|
for (const backup of completedBackups) {
|
|
14947
15197
|
try {
|
|
14948
|
-
|
|
15198
|
+
if (backup.isDir) {
|
|
15199
|
+
rmSync(backup.path, { recursive: true, force: true });
|
|
15200
|
+
} else {
|
|
15201
|
+
unlinkSync(backup.path);
|
|
15202
|
+
}
|
|
14949
15203
|
} catch {}
|
|
14950
15204
|
}
|
|
14951
15205
|
} catch (error) {
|
|
14952
15206
|
for (const rename2 of completedRenames) {
|
|
14953
15207
|
try {
|
|
14954
15208
|
if (existsSync7(rename2.destination)) {
|
|
14955
|
-
|
|
15209
|
+
if (rename2.isDir) {
|
|
15210
|
+
rmSync(rename2.destination, { recursive: true, force: true });
|
|
15211
|
+
} else {
|
|
15212
|
+
unlinkSync(rename2.destination);
|
|
15213
|
+
}
|
|
14956
15214
|
}
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
if (
|
|
15215
|
+
mkdirSync(path6.dirname(rename2.source), { recursive: true });
|
|
15216
|
+
if (existsSync7(rename2.backup)) {
|
|
15217
|
+
if (rename2.isDir) {
|
|
15218
|
+
cpSync(rename2.backup, rename2.source, { recursive: true });
|
|
15219
|
+
} else {
|
|
14960
15220
|
copyFileSync(rename2.backup, rename2.source);
|
|
14961
15221
|
}
|
|
14962
|
-
}
|
|
14963
|
-
}
|
|
15222
|
+
}
|
|
15223
|
+
} catch {}
|
|
14964
15224
|
}
|
|
14965
15225
|
for (const backup of completedBackups) {
|
|
14966
15226
|
try {
|
|
14967
|
-
if (existsSync7(backup)) {
|
|
14968
|
-
|
|
15227
|
+
if (existsSync7(backup.path)) {
|
|
15228
|
+
if (backup.isDir) {
|
|
15229
|
+
rmSync(backup.path, { recursive: true, force: true });
|
|
15230
|
+
} else {
|
|
15231
|
+
unlinkSync(backup.path);
|
|
15232
|
+
}
|
|
14969
15233
|
}
|
|
14970
15234
|
} catch {}
|
|
14971
15235
|
}
|
|
14972
15236
|
throw error;
|
|
14973
15237
|
}
|
|
15238
|
+
try {
|
|
15239
|
+
const processedProfiles = new Set(plan.profiles.filter((a) => a.type === "move-dir").map((a) => a.profileName));
|
|
15240
|
+
for (const profileName of processedProfiles) {
|
|
15241
|
+
const profileDir = path6.join(getProfilesDir(), profileName);
|
|
15242
|
+
const dotOpencode = path6.join(profileDir, ".opencode");
|
|
15243
|
+
if (existsSync7(dotOpencode)) {
|
|
15244
|
+
const remaining = readdirSync(dotOpencode);
|
|
15245
|
+
if (remaining.length === 0) {
|
|
15246
|
+
try {
|
|
15247
|
+
rmdirSync(dotOpencode);
|
|
15248
|
+
} catch {}
|
|
15249
|
+
}
|
|
15250
|
+
}
|
|
15251
|
+
}
|
|
15252
|
+
} catch {}
|
|
14974
15253
|
}
|
|
14975
15254
|
function printPlan(plan, dryRun) {
|
|
14976
15255
|
const prefix = dryRun ? "[DRY-RUN] " : "";
|
|
@@ -14983,7 +15262,12 @@ function printPlan(plan, dryRun) {
|
|
|
14983
15262
|
if (plan.profiles.length > 0 || plan.skipped.length > 0) {
|
|
14984
15263
|
console.log("Profiles:");
|
|
14985
15264
|
for (const action of plan.profiles) {
|
|
14986
|
-
|
|
15265
|
+
if (action.type === "rename") {
|
|
15266
|
+
console.log(` \u2713 ${action.profileName}: ${GHOST_CONFIG_FILE} \u2192 ${OCX_CONFIG_FILE}`);
|
|
15267
|
+
} else {
|
|
15268
|
+
const dirName = path6.basename(action.source);
|
|
15269
|
+
console.log(` \u2713 ${action.profileName}: .opencode/${dirName}/ \u2192 ${dirName}/`);
|
|
15270
|
+
}
|
|
14987
15271
|
}
|
|
14988
15272
|
for (const skipped of plan.skipped) {
|
|
14989
15273
|
console.log(` \u26A0 ${skipped.profileName}: skipped (${skipped.reason})`);
|
|
@@ -15099,34 +15383,90 @@ async function runInit(options2) {
|
|
|
15099
15383
|
}
|
|
15100
15384
|
}
|
|
15101
15385
|
async function runInitGlobal(options2) {
|
|
15102
|
-
const manager = ProfileManager.create();
|
|
15103
|
-
if (await manager.isInitialized()) {
|
|
15104
|
-
const profilesDir = getProfilesDir();
|
|
15105
|
-
throw new ProfileExistsError(`Global profiles already initialized at ${profilesDir}`);
|
|
15106
|
-
}
|
|
15107
15386
|
const spin = options2.quiet ? null : createSpinner({ text: "Initializing global profiles..." });
|
|
15108
15387
|
spin?.start();
|
|
15109
15388
|
try {
|
|
15110
|
-
|
|
15389
|
+
const created = [];
|
|
15390
|
+
const existed = [];
|
|
15391
|
+
const globalConfigPath = getGlobalConfig();
|
|
15392
|
+
if (existsSync8(globalConfigPath)) {
|
|
15393
|
+
existed.push("globalConfig");
|
|
15394
|
+
} else {
|
|
15395
|
+
await mkdir7(dirname3(globalConfigPath), { recursive: true, mode: 448 });
|
|
15396
|
+
await atomicWrite(globalConfigPath, {
|
|
15397
|
+
$schema: OCX_SCHEMA_URL,
|
|
15398
|
+
registries: {}
|
|
15399
|
+
});
|
|
15400
|
+
created.push("globalConfig");
|
|
15401
|
+
}
|
|
15111
15402
|
const profilesDir = getProfilesDir();
|
|
15112
|
-
|
|
15403
|
+
if (!existsSync8(profilesDir)) {
|
|
15404
|
+
await mkdir7(profilesDir, { recursive: true, mode: 448 });
|
|
15405
|
+
}
|
|
15406
|
+
const profileDir = getProfileDir("default");
|
|
15407
|
+
if (!existsSync8(profileDir)) {
|
|
15408
|
+
await mkdir7(profileDir, { recursive: true, mode: 448 });
|
|
15409
|
+
}
|
|
15410
|
+
const ocxPath = getProfileOcxConfig("default");
|
|
15411
|
+
if (existsSync8(ocxPath)) {
|
|
15412
|
+
existed.push("profileOcx");
|
|
15413
|
+
} else {
|
|
15414
|
+
await atomicWrite(ocxPath, DEFAULT_OCX_CONFIG);
|
|
15415
|
+
created.push("profileOcx");
|
|
15416
|
+
}
|
|
15417
|
+
const opencodePath = getProfileOpencodeConfig("default");
|
|
15418
|
+
if (existsSync8(opencodePath)) {
|
|
15419
|
+
existed.push("profileOpencode");
|
|
15420
|
+
} else {
|
|
15421
|
+
await atomicWrite(opencodePath, {});
|
|
15422
|
+
created.push("profileOpencode");
|
|
15423
|
+
}
|
|
15424
|
+
const agentsPath = getProfileAgents("default");
|
|
15425
|
+
if (existsSync8(agentsPath)) {
|
|
15426
|
+
existed.push("profileAgents");
|
|
15427
|
+
} else {
|
|
15428
|
+
const agentsContent = `# Profile Instructions
|
|
15429
|
+
|
|
15430
|
+
<!-- Add your custom instructions for this profile here -->
|
|
15431
|
+
<!-- These will be included when running \`ocx opencode -p default\` -->
|
|
15432
|
+
`;
|
|
15433
|
+
await Bun.write(agentsPath, agentsContent, { mode: 384 });
|
|
15434
|
+
created.push("profileAgents");
|
|
15435
|
+
}
|
|
15113
15436
|
spin?.succeed("Initialized global profiles");
|
|
15114
15437
|
if (options2.json) {
|
|
15115
15438
|
console.log(JSON.stringify({
|
|
15116
15439
|
success: true,
|
|
15117
|
-
|
|
15118
|
-
|
|
15119
|
-
|
|
15440
|
+
files: {
|
|
15441
|
+
globalConfig: globalConfigPath,
|
|
15442
|
+
profileOcx: ocxPath,
|
|
15443
|
+
profileOpencode: opencodePath,
|
|
15444
|
+
profileAgents: agentsPath
|
|
15445
|
+
},
|
|
15446
|
+
created,
|
|
15447
|
+
existed
|
|
15120
15448
|
}));
|
|
15121
15449
|
} else if (!options2.quiet) {
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15450
|
+
if (created.length > 0) {
|
|
15451
|
+
for (const key of created) {
|
|
15452
|
+
if (key === "globalConfig")
|
|
15453
|
+
logger.info(`Created global config: ${globalConfigPath}`);
|
|
15454
|
+
if (key === "profileOcx")
|
|
15455
|
+
logger.info(`Created profile config: ${ocxPath}`);
|
|
15456
|
+
if (key === "profileOpencode")
|
|
15457
|
+
logger.info(`Created profile opencode config: ${opencodePath}`);
|
|
15458
|
+
if (key === "profileAgents")
|
|
15459
|
+
logger.info(`Created profile instructions: ${agentsPath}`);
|
|
15460
|
+
}
|
|
15461
|
+
logger.info("");
|
|
15462
|
+
logger.info("Next steps:");
|
|
15463
|
+
logger.info(" 1. Edit your profile config: ocx config edit -p default");
|
|
15464
|
+
logger.info(" 2. Add registries: ocx registry add <url> --name <name> --global");
|
|
15465
|
+
logger.info(" 3. Launch OpenCode: ocx opencode");
|
|
15466
|
+
logger.info(" 4. Create more profiles: ocx profile add <name>");
|
|
15467
|
+
} else {
|
|
15468
|
+
logger.info("Global profiles already initialized (all files exist)");
|
|
15469
|
+
}
|
|
15130
15470
|
}
|
|
15131
15471
|
} catch (error) {
|
|
15132
15472
|
spin?.fail("Failed to initialize");
|
|
@@ -15154,7 +15494,7 @@ async function runInitRegistry(directory, options2) {
|
|
|
15154
15494
|
await mkdir7(cwd, { recursive: true });
|
|
15155
15495
|
await copyDir(options2.local, cwd);
|
|
15156
15496
|
} else {
|
|
15157
|
-
const version = options2.canary ? "main" :
|
|
15497
|
+
const version = options2.canary ? "main" : getReleaseTag();
|
|
15158
15498
|
await fetchAndExtractTemplate(cwd, version, options2.verbose);
|
|
15159
15499
|
}
|
|
15160
15500
|
if (spin)
|
|
@@ -15183,15 +15523,16 @@ async function runInitRegistry(directory, options2) {
|
|
|
15183
15523
|
async function copyDir(src, dest) {
|
|
15184
15524
|
await cp(src, dest, { recursive: true });
|
|
15185
15525
|
}
|
|
15186
|
-
|
|
15187
|
-
|
|
15188
|
-
|
|
15189
|
-
const pkg = JSON.parse(pkgContent.toString());
|
|
15190
|
-
return `v${pkg.version}`;
|
|
15526
|
+
function getReleaseTag() {
|
|
15527
|
+
if (false) {}
|
|
15528
|
+
return `v${"1.4.2"}`;
|
|
15191
15529
|
}
|
|
15192
|
-
|
|
15530
|
+
function getTemplateUrl(version) {
|
|
15193
15531
|
const ref = version === "main" ? "heads/main" : `tags/${version}`;
|
|
15194
|
-
|
|
15532
|
+
return `https://github.com/${TEMPLATE_REPO}/archive/refs/${ref}.tar.gz`;
|
|
15533
|
+
}
|
|
15534
|
+
async function fetchAndExtractTemplate(destDir, version, verbose) {
|
|
15535
|
+
const tarballUrl = getTemplateUrl(version);
|
|
15195
15536
|
if (verbose) {
|
|
15196
15537
|
logger.info(`Fetching ${tarballUrl}`);
|
|
15197
15538
|
}
|
|
@@ -15300,6 +15641,18 @@ function formatTerminalName(cwd, profileName, gitInfo) {
|
|
|
15300
15641
|
}
|
|
15301
15642
|
|
|
15302
15643
|
// src/commands/opencode.ts
|
|
15644
|
+
function resolveOpenCodeBinary(opts) {
|
|
15645
|
+
return opts.configBin ?? opts.envBin ?? "opencode";
|
|
15646
|
+
}
|
|
15647
|
+
function buildOpenCodeEnv(opts) {
|
|
15648
|
+
return {
|
|
15649
|
+
...opts.baseEnv,
|
|
15650
|
+
...opts.disableProjectConfig && { OPENCODE_DISABLE_PROJECT_CONFIG: "true" },
|
|
15651
|
+
...opts.profileDir && { OPENCODE_CONFIG_DIR: opts.profileDir },
|
|
15652
|
+
...opts.mergedConfig && { OPENCODE_CONFIG_CONTENT: JSON.stringify(opts.mergedConfig) },
|
|
15653
|
+
...opts.profileName && { OCX_PROFILE: opts.profileName }
|
|
15654
|
+
};
|
|
15655
|
+
}
|
|
15303
15656
|
function registerOpencodeCommand(program2) {
|
|
15304
15657
|
program2.command("opencode [path]").alias("oc").description("Launch OpenCode with resolved configuration").option("-p, --profile <name>", "Use specific profile").option("--no-rename", "Disable terminal/tmux window renaming").addOption(sharedOptions.quiet()).addOption(sharedOptions.json()).allowUnknownOption().allowExcessArguments(true).action(async (path8, options2, command) => {
|
|
15305
15658
|
try {
|
|
@@ -15354,17 +15707,20 @@ async function runOpencode(pathArg, args, options2) {
|
|
|
15354
15707
|
const gitInfo = await getGitInfo(projectDir);
|
|
15355
15708
|
setTerminalName(formatTerminalName(projectDir, config.profileName ?? "default", gitInfo));
|
|
15356
15709
|
}
|
|
15357
|
-
const bin =
|
|
15710
|
+
const bin = resolveOpenCodeBinary({
|
|
15711
|
+
configBin: ocxConfig?.bin,
|
|
15712
|
+
envBin: process.env.OPENCODE_BIN
|
|
15713
|
+
});
|
|
15358
15714
|
proc = Bun.spawn({
|
|
15359
15715
|
cmd: [bin, ...args],
|
|
15360
15716
|
cwd: projectDir,
|
|
15361
|
-
env: {
|
|
15362
|
-
|
|
15363
|
-
|
|
15364
|
-
|
|
15365
|
-
|
|
15366
|
-
|
|
15367
|
-
},
|
|
15717
|
+
env: buildOpenCodeEnv({
|
|
15718
|
+
baseEnv: process.env,
|
|
15719
|
+
profileDir,
|
|
15720
|
+
profileName: config.profileName ?? undefined,
|
|
15721
|
+
mergedConfig: configToPass,
|
|
15722
|
+
disableProjectConfig: true
|
|
15723
|
+
}),
|
|
15368
15724
|
stdin: "inherit",
|
|
15369
15725
|
stdout: "inherit",
|
|
15370
15726
|
stderr: "inherit"
|
|
@@ -15389,9 +15745,290 @@ async function runOpencode(pathArg, args, options2) {
|
|
|
15389
15745
|
}
|
|
15390
15746
|
}
|
|
15391
15747
|
|
|
15748
|
+
// src/commands/profile/install-from-registry.ts
|
|
15749
|
+
import { createHash as createHash2 } from "crypto";
|
|
15750
|
+
import { existsSync as existsSync9 } from "fs";
|
|
15751
|
+
import { mkdir as mkdir8, mkdtemp, rename as rename2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
15752
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
15753
|
+
var PROFILE_FILE_TARGETS = new Set(["ocx.jsonc", "opencode.jsonc", "AGENTS.md"]);
|
|
15754
|
+
function isProfileFile(target) {
|
|
15755
|
+
return PROFILE_FILE_TARGETS.has(target);
|
|
15756
|
+
}
|
|
15757
|
+
function hashContent2(content2) {
|
|
15758
|
+
return createHash2("sha256").update(content2).digest("hex");
|
|
15759
|
+
}
|
|
15760
|
+
function hashBundle2(files) {
|
|
15761
|
+
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
15762
|
+
const manifestParts = [];
|
|
15763
|
+
for (const file of sorted) {
|
|
15764
|
+
const hash = hashContent2(file.content);
|
|
15765
|
+
manifestParts.push(`${file.path}:${hash}`);
|
|
15766
|
+
}
|
|
15767
|
+
return hashContent2(manifestParts.join(`
|
|
15768
|
+
`));
|
|
15769
|
+
}
|
|
15770
|
+
async function installProfileFromRegistry(options2) {
|
|
15771
|
+
const { namespace, component, profileName, force, registryUrl, registries, quiet } = options2;
|
|
15772
|
+
const parseResult = profileNameSchema.safeParse(profileName);
|
|
15773
|
+
if (!parseResult.success) {
|
|
15774
|
+
throw new ValidationError(`Invalid profile name: "${profileName}". ` + `Profile names must start with a letter and contain only alphanumeric characters, dots, underscores, or hyphens.`);
|
|
15775
|
+
}
|
|
15776
|
+
const profileDir = getProfileDir(profileName);
|
|
15777
|
+
const qualifiedName = `${namespace}/${component}`;
|
|
15778
|
+
const profileExists = existsSync9(profileDir);
|
|
15779
|
+
if (profileExists && !force) {
|
|
15780
|
+
throw new ConflictError(`Profile "${profileName}" already exists.
|
|
15781
|
+
Use --force to overwrite.`);
|
|
15782
|
+
}
|
|
15783
|
+
const fetchSpin = quiet ? null : createSpinner({ text: `Fetching ${qualifiedName}...` });
|
|
15784
|
+
fetchSpin?.start();
|
|
15785
|
+
let manifest;
|
|
15786
|
+
try {
|
|
15787
|
+
manifest = await fetchComponent(registryUrl, component);
|
|
15788
|
+
} catch (error) {
|
|
15789
|
+
fetchSpin?.fail(`Failed to fetch ${qualifiedName}`);
|
|
15790
|
+
if (error instanceof NotFoundError) {
|
|
15791
|
+
throw new NotFoundError(`Profile component "${qualifiedName}" not found in registry.
|
|
15792
|
+
|
|
15793
|
+
` + `Check the component name and ensure the registry is configured.`);
|
|
15794
|
+
}
|
|
15795
|
+
throw error;
|
|
15796
|
+
}
|
|
15797
|
+
if (manifest.type !== "ocx:profile") {
|
|
15798
|
+
fetchSpin?.fail(`Invalid component type`);
|
|
15799
|
+
throw new ValidationError(`Component "${qualifiedName}" is type "${manifest.type}", not "ocx:profile".
|
|
15800
|
+
|
|
15801
|
+
` + `Only profile components can be installed with 'ocx profile add --from'.`);
|
|
15802
|
+
}
|
|
15803
|
+
const normalized = normalizeComponentManifest(manifest);
|
|
15804
|
+
fetchSpin?.succeed(`Fetched ${qualifiedName}`);
|
|
15805
|
+
const filesSpin = quiet ? null : createSpinner({ text: "Downloading profile files..." });
|
|
15806
|
+
filesSpin?.start();
|
|
15807
|
+
const profileFiles = [];
|
|
15808
|
+
const dependencyFiles = [];
|
|
15809
|
+
for (const file of normalized.files) {
|
|
15810
|
+
const content2 = await fetchFileContent(registryUrl, component, file.path);
|
|
15811
|
+
const fileEntry = {
|
|
15812
|
+
path: file.path,
|
|
15813
|
+
target: file.target,
|
|
15814
|
+
content: Buffer.from(content2)
|
|
15815
|
+
};
|
|
15816
|
+
if (isProfileFile(file.target)) {
|
|
15817
|
+
profileFiles.push(fileEntry);
|
|
15818
|
+
} else {
|
|
15819
|
+
dependencyFiles.push(fileEntry);
|
|
15820
|
+
}
|
|
15821
|
+
}
|
|
15822
|
+
filesSpin?.succeed(`Downloaded ${normalized.files.length} files`);
|
|
15823
|
+
let resolvedDeps = null;
|
|
15824
|
+
const dependencyBundles = [];
|
|
15825
|
+
if (manifest.dependencies.length > 0) {
|
|
15826
|
+
const depsSpin = quiet ? null : createSpinner({ text: "Resolving dependencies..." });
|
|
15827
|
+
depsSpin?.start();
|
|
15828
|
+
try {
|
|
15829
|
+
const depRefs = manifest.dependencies.map((dep) => dep.includes("/") ? dep : `${namespace}/${dep}`);
|
|
15830
|
+
resolvedDeps = await resolveDependencies(registries, depRefs);
|
|
15831
|
+
for (const depComponent of resolvedDeps.components) {
|
|
15832
|
+
const files = [];
|
|
15833
|
+
for (const file of depComponent.files) {
|
|
15834
|
+
const content2 = await fetchFileContent(depComponent.baseUrl, depComponent.name, file.path);
|
|
15835
|
+
const resolvedTarget = resolveTargetPath(file.target, true);
|
|
15836
|
+
files.push({
|
|
15837
|
+
path: file.path,
|
|
15838
|
+
target: resolvedTarget,
|
|
15839
|
+
content: Buffer.from(content2)
|
|
15840
|
+
});
|
|
15841
|
+
}
|
|
15842
|
+
const registryIndex = await fetchRegistryIndex(depComponent.baseUrl);
|
|
15843
|
+
dependencyBundles.push({
|
|
15844
|
+
qualifiedName: depComponent.qualifiedName,
|
|
15845
|
+
registryName: depComponent.registryName,
|
|
15846
|
+
files,
|
|
15847
|
+
hash: hashBundle2(files),
|
|
15848
|
+
version: registryIndex.version
|
|
15849
|
+
});
|
|
15850
|
+
}
|
|
15851
|
+
depsSpin?.succeed(`Resolved ${resolvedDeps.components.length} dependencies`);
|
|
15852
|
+
} catch (error) {
|
|
15853
|
+
depsSpin?.fail("Failed to resolve dependencies");
|
|
15854
|
+
throw error;
|
|
15855
|
+
}
|
|
15856
|
+
}
|
|
15857
|
+
const profilesDir = getProfilesDir();
|
|
15858
|
+
await mkdir8(profilesDir, { recursive: true, mode: 448 });
|
|
15859
|
+
const stagingDir = await mkdtemp(join8(profilesDir, ".staging-"));
|
|
15860
|
+
const stagingOpencodeDir = join8(stagingDir, ".opencode");
|
|
15861
|
+
try {
|
|
15862
|
+
await mkdir8(stagingOpencodeDir, { recursive: true, mode: 448 });
|
|
15863
|
+
const writeSpin = quiet ? null : createSpinner({ text: "Writing profile files..." });
|
|
15864
|
+
writeSpin?.start();
|
|
15865
|
+
for (const file of profileFiles) {
|
|
15866
|
+
const targetPath = join8(stagingDir, file.target);
|
|
15867
|
+
const targetDir = dirname4(targetPath);
|
|
15868
|
+
if (!existsSync9(targetDir)) {
|
|
15869
|
+
await mkdir8(targetDir, { recursive: true });
|
|
15870
|
+
}
|
|
15871
|
+
await writeFile3(targetPath, file.content);
|
|
15872
|
+
}
|
|
15873
|
+
for (const file of dependencyFiles) {
|
|
15874
|
+
const target = file.target.startsWith(".opencode/") ? file.target.slice(".opencode/".length) : file.target;
|
|
15875
|
+
const targetPath = join8(stagingOpencodeDir, target);
|
|
15876
|
+
const targetDir = dirname4(targetPath);
|
|
15877
|
+
if (!existsSync9(targetDir)) {
|
|
15878
|
+
await mkdir8(targetDir, { recursive: true });
|
|
15879
|
+
}
|
|
15880
|
+
await writeFile3(targetPath, file.content);
|
|
15881
|
+
}
|
|
15882
|
+
writeSpin?.succeed(`Wrote ${profileFiles.length + dependencyFiles.length} profile files`);
|
|
15883
|
+
if (dependencyBundles.length > 0) {
|
|
15884
|
+
const depWriteSpin = quiet ? null : createSpinner({ text: "Writing dependency files..." });
|
|
15885
|
+
depWriteSpin?.start();
|
|
15886
|
+
let depFileCount = 0;
|
|
15887
|
+
for (const bundle of dependencyBundles) {
|
|
15888
|
+
for (const file of bundle.files) {
|
|
15889
|
+
const targetPath = join8(stagingOpencodeDir, file.target);
|
|
15890
|
+
const targetDir = dirname4(targetPath);
|
|
15891
|
+
if (!existsSync9(targetDir)) {
|
|
15892
|
+
await mkdir8(targetDir, { recursive: true });
|
|
15893
|
+
}
|
|
15894
|
+
await writeFile3(targetPath, file.content);
|
|
15895
|
+
depFileCount++;
|
|
15896
|
+
}
|
|
15897
|
+
}
|
|
15898
|
+
depWriteSpin?.succeed(`Wrote ${depFileCount} dependency files`);
|
|
15899
|
+
}
|
|
15900
|
+
const profileHash = hashBundle2(profileFiles.map((f) => ({ path: f.path, content: f.content })));
|
|
15901
|
+
const registryIndex = await fetchRegistryIndex(registryUrl);
|
|
15902
|
+
const lock = {
|
|
15903
|
+
version: 1,
|
|
15904
|
+
installedFrom: {
|
|
15905
|
+
registry: namespace,
|
|
15906
|
+
component,
|
|
15907
|
+
version: registryIndex.version,
|
|
15908
|
+
hash: profileHash,
|
|
15909
|
+
installedAt: new Date().toISOString()
|
|
15910
|
+
},
|
|
15911
|
+
installed: {}
|
|
15912
|
+
};
|
|
15913
|
+
for (const bundle of dependencyBundles) {
|
|
15914
|
+
lock.installed[bundle.qualifiedName] = {
|
|
15915
|
+
registry: bundle.registryName,
|
|
15916
|
+
version: bundle.version,
|
|
15917
|
+
hash: bundle.hash,
|
|
15918
|
+
files: bundle.files.map((f) => f.target),
|
|
15919
|
+
installedAt: new Date().toISOString()
|
|
15920
|
+
};
|
|
15921
|
+
}
|
|
15922
|
+
await writeFile3(join8(stagingDir, "ocx.lock"), JSON.stringify(lock, null, "\t"));
|
|
15923
|
+
const moveSpin = quiet ? null : createSpinner({ text: "Finalizing installation..." });
|
|
15924
|
+
moveSpin?.start();
|
|
15925
|
+
const profilesDir2 = dirname4(profileDir);
|
|
15926
|
+
if (!existsSync9(profilesDir2)) {
|
|
15927
|
+
await mkdir8(profilesDir2, { recursive: true, mode: 448 });
|
|
15928
|
+
}
|
|
15929
|
+
if (profileExists && force) {
|
|
15930
|
+
const backupDir = `${profileDir}.backup-${Date.now()}`;
|
|
15931
|
+
await rename2(profileDir, backupDir);
|
|
15932
|
+
try {
|
|
15933
|
+
await rename2(stagingDir, profileDir);
|
|
15934
|
+
} catch (err) {
|
|
15935
|
+
await rename2(backupDir, profileDir);
|
|
15936
|
+
throw err;
|
|
15937
|
+
}
|
|
15938
|
+
await rm3(backupDir, { recursive: true, force: true });
|
|
15939
|
+
} else {
|
|
15940
|
+
await rename2(stagingDir, profileDir);
|
|
15941
|
+
}
|
|
15942
|
+
moveSpin?.succeed("Installation complete");
|
|
15943
|
+
if (!quiet) {
|
|
15944
|
+
logger.info("");
|
|
15945
|
+
logger.success(`Installed profile "${profileName}" from ${qualifiedName}`);
|
|
15946
|
+
logger.info("");
|
|
15947
|
+
logger.info("Profile contents:");
|
|
15948
|
+
for (const file of profileFiles) {
|
|
15949
|
+
logger.info(` ${file.target}`);
|
|
15950
|
+
}
|
|
15951
|
+
if (dependencyBundles.length > 0) {
|
|
15952
|
+
logger.info("");
|
|
15953
|
+
logger.info("Dependencies:");
|
|
15954
|
+
for (const bundle of dependencyBundles) {
|
|
15955
|
+
logger.info(` ${bundle.qualifiedName}`);
|
|
15956
|
+
}
|
|
15957
|
+
}
|
|
15958
|
+
}
|
|
15959
|
+
} catch (error) {
|
|
15960
|
+
try {
|
|
15961
|
+
if (existsSync9(stagingDir)) {
|
|
15962
|
+
await rm3(stagingDir, { recursive: true });
|
|
15963
|
+
}
|
|
15964
|
+
} catch {}
|
|
15965
|
+
throw error;
|
|
15966
|
+
}
|
|
15967
|
+
}
|
|
15968
|
+
|
|
15392
15969
|
// src/commands/profile/add.ts
|
|
15970
|
+
function parseFromOption(from) {
|
|
15971
|
+
if (!from?.trim()) {
|
|
15972
|
+
throw new ValidationError("--from value cannot be empty");
|
|
15973
|
+
}
|
|
15974
|
+
const trimmed = from.trim();
|
|
15975
|
+
if (trimmed.startsWith("./") || trimmed.startsWith("~/") || trimmed.startsWith("/")) {
|
|
15976
|
+
return { type: "local-path", path: trimmed };
|
|
15977
|
+
}
|
|
15978
|
+
const slashCount = (trimmed.match(/\//g) || []).length;
|
|
15979
|
+
if (slashCount === 1) {
|
|
15980
|
+
const [namespace, component] = trimmed.split("/").map((s) => s.trim());
|
|
15981
|
+
if (!namespace || !component) {
|
|
15982
|
+
throw new ValidationError(`Invalid registry reference: "${from}". Expected format: namespace/component`);
|
|
15983
|
+
}
|
|
15984
|
+
return { type: "registry", namespace, component };
|
|
15985
|
+
}
|
|
15986
|
+
return { type: "local-profile", name: trimmed };
|
|
15987
|
+
}
|
|
15988
|
+
async function readGlobalOcxConfig() {
|
|
15989
|
+
const configPath = getGlobalConfig();
|
|
15990
|
+
const file = Bun.file(configPath);
|
|
15991
|
+
if (!await file.exists()) {
|
|
15992
|
+
return null;
|
|
15993
|
+
}
|
|
15994
|
+
try {
|
|
15995
|
+
const content2 = await file.text();
|
|
15996
|
+
const json = parse2(content2, [], { allowTrailingComma: true });
|
|
15997
|
+
return profileOcxConfigSchema.parse(json);
|
|
15998
|
+
} catch (error) {
|
|
15999
|
+
if (error instanceof Error) {
|
|
16000
|
+
throw new ConfigError(`Failed to parse global config at "${configPath}": ${error.message}
|
|
16001
|
+
|
|
16002
|
+
` + `Check the file for syntax errors or run: ocx config edit --global`);
|
|
16003
|
+
}
|
|
16004
|
+
throw error;
|
|
16005
|
+
}
|
|
16006
|
+
}
|
|
16007
|
+
async function requireGlobalRegistry(namespace) {
|
|
16008
|
+
const globalConfig = await readGlobalOcxConfig();
|
|
16009
|
+
if (!globalConfig) {
|
|
16010
|
+
throw new ConfigError(`Registry "${namespace}" is not configured globally.
|
|
16011
|
+
|
|
16012
|
+
` + `Profile installation requires global registry configuration.
|
|
16013
|
+
` + `Run: ocx registry add <url> --name ${namespace} --global`);
|
|
16014
|
+
}
|
|
16015
|
+
const registry = globalConfig.registries[namespace];
|
|
16016
|
+
if (!registry) {
|
|
16017
|
+
throw new ConfigError(`Registry "${namespace}" is not configured globally.
|
|
16018
|
+
|
|
16019
|
+
` + `Profile installation requires global registry configuration.
|
|
16020
|
+
` + `Run: ocx registry add <url> --name ${namespace} --global`);
|
|
16021
|
+
}
|
|
16022
|
+
return { config: globalConfig, registryUrl: registry.url };
|
|
16023
|
+
}
|
|
15393
16024
|
function registerProfileAddCommand(parent) {
|
|
15394
|
-
parent.command("add <name>").description("Create a new
|
|
16025
|
+
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", `
|
|
16026
|
+
Examples:
|
|
16027
|
+
$ ocx profile add work # Create empty profile
|
|
16028
|
+
$ ocx profile add work --from dev # Clone from existing profile
|
|
16029
|
+
$ ocx profile add work --from kdco/minimal # Install from registry
|
|
16030
|
+
$ ocx profile add work --from kdco/minimal --force # Overwrite existing
|
|
16031
|
+
`).action(async (name, options2) => {
|
|
15395
16032
|
try {
|
|
15396
16033
|
await runProfileAdd(name, options2);
|
|
15397
16034
|
} catch (error) {
|
|
@@ -15401,42 +16038,61 @@ function registerProfileAddCommand(parent) {
|
|
|
15401
16038
|
}
|
|
15402
16039
|
async function runProfileAdd(name, options2) {
|
|
15403
16040
|
const manager = await ProfileManager.requireInitialized();
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
logger.
|
|
15409
|
-
|
|
15410
|
-
|
|
15411
|
-
|
|
16041
|
+
const profileExists = await manager.exists(name);
|
|
16042
|
+
if (profileExists && !options2.force) {
|
|
16043
|
+
logger.error(`\u2717 Profile "${name}" already exists`);
|
|
16044
|
+
logger.error("");
|
|
16045
|
+
logger.error("Use --force to overwrite the existing profile.");
|
|
16046
|
+
throw new ProfileExistsError(name);
|
|
16047
|
+
}
|
|
16048
|
+
if (!options2.from) {
|
|
16049
|
+
await createEmptyProfile(manager, name, profileExists);
|
|
16050
|
+
return;
|
|
15412
16051
|
}
|
|
15413
|
-
|
|
16052
|
+
const fromInput = parseFromOption(options2.from);
|
|
16053
|
+
switch (fromInput.type) {
|
|
16054
|
+
case "local-profile":
|
|
16055
|
+
await cloneFromLocalProfile(manager, name, fromInput.name, profileExists);
|
|
16056
|
+
break;
|
|
16057
|
+
case "local-path":
|
|
16058
|
+
throw new ValidationError(`Local path installation is not yet implemented: "${fromInput.path}"
|
|
15414
16059
|
|
|
15415
|
-
|
|
15416
|
-
|
|
15417
|
-
|
|
15418
|
-
|
|
15419
|
-
await
|
|
15420
|
-
|
|
15421
|
-
|
|
16060
|
+
` + `Currently supported sources:
|
|
16061
|
+
` + ` - Existing profile: --from <profile-name>
|
|
16062
|
+
` + ` - Registry: --from <namespace>/<component>`);
|
|
16063
|
+
case "registry": {
|
|
16064
|
+
const { config: globalConfig, registryUrl } = await requireGlobalRegistry(fromInput.namespace);
|
|
16065
|
+
const registries = {};
|
|
16066
|
+
for (const [ns, reg] of Object.entries(globalConfig.registries)) {
|
|
16067
|
+
registries[ns] = { url: reg.url };
|
|
16068
|
+
}
|
|
16069
|
+
await installProfileFromRegistry({
|
|
16070
|
+
namespace: fromInput.namespace,
|
|
16071
|
+
component: fromInput.component,
|
|
16072
|
+
profileName: name,
|
|
16073
|
+
force: options2.force,
|
|
16074
|
+
registryUrl,
|
|
16075
|
+
registries
|
|
16076
|
+
});
|
|
16077
|
+
break;
|
|
15422
16078
|
}
|
|
15423
|
-
}
|
|
16079
|
+
}
|
|
15424
16080
|
}
|
|
15425
|
-
async function
|
|
15426
|
-
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
const exitCode = await proc.exited;
|
|
15437
|
-
if (exitCode !== 0) {
|
|
15438
|
-
throw new Error(`Editor exited with code ${exitCode}`);
|
|
16081
|
+
async function createEmptyProfile(manager, name, exists) {
|
|
16082
|
+
if (exists) {
|
|
16083
|
+
await manager.remove(name);
|
|
16084
|
+
}
|
|
16085
|
+
await manager.add(name);
|
|
16086
|
+
logger.success(`Created profile "${name}"`);
|
|
16087
|
+
}
|
|
16088
|
+
async function cloneFromLocalProfile(manager, name, sourceName, exists) {
|
|
16089
|
+
const source = await manager.get(sourceName);
|
|
16090
|
+
if (exists) {
|
|
16091
|
+
await manager.remove(name);
|
|
15439
16092
|
}
|
|
16093
|
+
await manager.add(name);
|
|
16094
|
+
await atomicWrite(getProfileOcxConfig(name), source.ocx);
|
|
16095
|
+
logger.success(`Created profile "${name}" (cloned from "${sourceName}")`);
|
|
15440
16096
|
}
|
|
15441
16097
|
|
|
15442
16098
|
// src/commands/profile/list.ts
|
|
@@ -15525,22 +16181,43 @@ function registerProfileCommand(program2) {
|
|
|
15525
16181
|
registerProfileAddCommand(profile);
|
|
15526
16182
|
registerProfileRemoveCommand(profile);
|
|
15527
16183
|
registerProfileShowCommand(profile);
|
|
15528
|
-
registerProfileConfigCommand(profile);
|
|
15529
16184
|
}
|
|
15530
16185
|
|
|
15531
16186
|
// src/commands/registry.ts
|
|
16187
|
+
import { existsSync as existsSync10 } from "fs";
|
|
16188
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
15532
16189
|
async function runRegistryAddCore2(url, options2, callbacks) {
|
|
15533
16190
|
if (callbacks.isLocked?.()) {
|
|
15534
16191
|
throw new Error("Registries are locked. Cannot add.");
|
|
15535
16192
|
}
|
|
15536
|
-
const
|
|
16193
|
+
const trimmedUrl = url.trim();
|
|
16194
|
+
if (!trimmedUrl) {
|
|
16195
|
+
throw new ValidationError("Registry URL is required");
|
|
16196
|
+
}
|
|
16197
|
+
let derivedName;
|
|
16198
|
+
try {
|
|
16199
|
+
const parsed = new URL(trimmedUrl);
|
|
16200
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
16201
|
+
throw new ValidationError(`Invalid registry URL: ${trimmedUrl} (must use http or https)`);
|
|
16202
|
+
}
|
|
16203
|
+
derivedName = options2.name || parsed.hostname.replace(/\./g, "-");
|
|
16204
|
+
} catch (error) {
|
|
16205
|
+
if (error instanceof ValidationError)
|
|
16206
|
+
throw error;
|
|
16207
|
+
throw new ValidationError(`Invalid registry URL: ${trimmedUrl}`);
|
|
16208
|
+
}
|
|
16209
|
+
const name = derivedName;
|
|
15537
16210
|
const registries = callbacks.getRegistries();
|
|
16211
|
+
const existingRegistry = registries[name];
|
|
16212
|
+
if (existingRegistry && !options2.force) {
|
|
16213
|
+
throw new RegistryExistsError(name, existingRegistry.url, trimmedUrl);
|
|
16214
|
+
}
|
|
15538
16215
|
const isUpdate = name in registries;
|
|
15539
16216
|
await callbacks.setRegistry(name, {
|
|
15540
|
-
url,
|
|
16217
|
+
url: trimmedUrl,
|
|
15541
16218
|
version: options2.version
|
|
15542
16219
|
});
|
|
15543
|
-
return { name, url, updated: isUpdate };
|
|
16220
|
+
return { name, url: trimmedUrl, updated: isUpdate };
|
|
15544
16221
|
}
|
|
15545
16222
|
async function runRegistryRemoveCore(name, callbacks) {
|
|
15546
16223
|
if (callbacks.isLocked?.()) {
|
|
@@ -15563,17 +16240,67 @@ function runRegistryListCore(callbacks) {
|
|
|
15563
16240
|
}));
|
|
15564
16241
|
return { registries: list, locked };
|
|
15565
16242
|
}
|
|
16243
|
+
async function resolveRegistryTarget(options2, command, cwd) {
|
|
16244
|
+
const cwdExplicitlyProvided = command.getOptionValueSource("cwd") === "cli";
|
|
16245
|
+
if (options2.global && options2.profile) {
|
|
16246
|
+
throw new ValidationError("Cannot use both --global and --profile flags");
|
|
16247
|
+
}
|
|
16248
|
+
if (cwdExplicitlyProvided && options2.profile) {
|
|
16249
|
+
throw new ValidationError("Cannot use both --cwd and --profile flags");
|
|
16250
|
+
}
|
|
16251
|
+
if (options2.global && cwdExplicitlyProvided) {
|
|
16252
|
+
throw new ValidationError("Cannot use both --global and --cwd flags");
|
|
16253
|
+
}
|
|
16254
|
+
if (options2.profile) {
|
|
16255
|
+
validateProfileName(options2.profile);
|
|
16256
|
+
const manager = await ProfileManager.requireInitialized();
|
|
16257
|
+
if (!await manager.exists(options2.profile)) {
|
|
16258
|
+
throw new ProfileNotFoundError(options2.profile);
|
|
16259
|
+
}
|
|
16260
|
+
const configPath = getProfileOcxConfig(options2.profile);
|
|
16261
|
+
if (!existsSync10(configPath)) {
|
|
16262
|
+
throw new OcxConfigError(`Profile '${options2.profile}' has no ocx.jsonc. Run 'ocx profile config ${options2.profile}' to create it.`);
|
|
16263
|
+
}
|
|
16264
|
+
return {
|
|
16265
|
+
scope: "profile",
|
|
16266
|
+
configPath,
|
|
16267
|
+
configDir: dirname5(configPath),
|
|
16268
|
+
targetLabel: `profile '${options2.profile}' config`
|
|
16269
|
+
};
|
|
16270
|
+
}
|
|
16271
|
+
if (options2.global) {
|
|
16272
|
+
const configDir = getGlobalConfigPath();
|
|
16273
|
+
return {
|
|
16274
|
+
scope: "global",
|
|
16275
|
+
configPath: join9(configDir, "ocx.jsonc"),
|
|
16276
|
+
configDir,
|
|
16277
|
+
targetLabel: "global config"
|
|
16278
|
+
};
|
|
16279
|
+
}
|
|
16280
|
+
const found = findOcxConfig(cwd);
|
|
16281
|
+
return {
|
|
16282
|
+
scope: "local",
|
|
16283
|
+
configPath: found.path,
|
|
16284
|
+
configDir: found.exists ? dirname5(found.path) : join9(cwd, ".opencode"),
|
|
16285
|
+
targetLabel: "local config"
|
|
16286
|
+
};
|
|
16287
|
+
}
|
|
15566
16288
|
function registerRegistryCommand(program2) {
|
|
15567
16289
|
const registry = program2.command("registry").description("Manage registries");
|
|
15568
|
-
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");
|
|
16290
|
+
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").option("-f, --force", "Overwrite existing registry");
|
|
16291
|
+
addGlobalOption(addCmd);
|
|
16292
|
+
addProfileOption(addCmd);
|
|
15569
16293
|
addCommonOptions(addCmd);
|
|
15570
|
-
addCmd.action(async (url, options2) => {
|
|
16294
|
+
addCmd.action(async (url, options2, command) => {
|
|
16295
|
+
let target;
|
|
15571
16296
|
try {
|
|
15572
16297
|
const cwd = options2.cwd ?? process.cwd();
|
|
15573
|
-
|
|
15574
|
-
const
|
|
16298
|
+
target = await resolveRegistryTarget(options2, command, cwd);
|
|
16299
|
+
const { configDir, configPath } = target;
|
|
16300
|
+
const config = await readOcxConfig(configDir);
|
|
15575
16301
|
if (!config) {
|
|
15576
|
-
|
|
16302
|
+
const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
|
|
16303
|
+
logger.error(`${target.targetLabel} not found. ${initHint}`);
|
|
15577
16304
|
process.exit(1);
|
|
15578
16305
|
}
|
|
15579
16306
|
const result = await runRegistryAddCore2(url, options2, {
|
|
@@ -15581,31 +16308,38 @@ function registerRegistryCommand(program2) {
|
|
|
15581
16308
|
isLocked: () => config.lockRegistries ?? false,
|
|
15582
16309
|
setRegistry: async (name, regConfig) => {
|
|
15583
16310
|
config.registries[name] = regConfig;
|
|
15584
|
-
await writeOcxConfig(
|
|
16311
|
+
await writeOcxConfig(configDir, config, configPath);
|
|
15585
16312
|
}
|
|
15586
16313
|
});
|
|
15587
16314
|
if (options2.json) {
|
|
15588
16315
|
outputJson({ success: true, data: result });
|
|
15589
16316
|
} else if (!options2.quiet) {
|
|
15590
16317
|
if (result.updated) {
|
|
15591
|
-
logger.success(`Updated registry: ${result.name} -> ${result.url}`);
|
|
16318
|
+
logger.success(`Updated registry in ${target.targetLabel}: ${result.name} -> ${result.url}`);
|
|
15592
16319
|
} else {
|
|
15593
|
-
logger.success(`Added registry: ${result.name} -> ${result.url}`);
|
|
16320
|
+
logger.success(`Added registry to ${target.targetLabel}: ${result.name} -> ${result.url}`);
|
|
15594
16321
|
}
|
|
15595
16322
|
}
|
|
15596
16323
|
} catch (error) {
|
|
16324
|
+
if (error instanceof RegistryExistsError && !error.targetLabel) {
|
|
16325
|
+
const enrichedError = new RegistryExistsError(error.registryName, error.existingUrl, error.newUrl, target?.targetLabel ?? "config");
|
|
16326
|
+
handleError(enrichedError, { json: options2.json });
|
|
16327
|
+
}
|
|
15597
16328
|
handleError(error, { json: options2.json });
|
|
15598
16329
|
}
|
|
15599
16330
|
});
|
|
15600
16331
|
const removeCmd = registry.command("remove").description("Remove a registry").argument("<name>", "Registry name");
|
|
16332
|
+
addGlobalOption(removeCmd);
|
|
16333
|
+
addProfileOption(removeCmd);
|
|
15601
16334
|
addCommonOptions(removeCmd);
|
|
15602
|
-
removeCmd.action(async (name, options2) => {
|
|
16335
|
+
removeCmd.action(async (name, options2, command) => {
|
|
15603
16336
|
try {
|
|
15604
16337
|
const cwd = options2.cwd ?? process.cwd();
|
|
15605
|
-
const
|
|
15606
|
-
const config = await readOcxConfig(
|
|
16338
|
+
const target = await resolveRegistryTarget(options2, command, cwd);
|
|
16339
|
+
const config = await readOcxConfig(target.configDir);
|
|
15607
16340
|
if (!config) {
|
|
15608
|
-
|
|
16341
|
+
const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
|
|
16342
|
+
logger.error(`${target.targetLabel} not found. ${initHint}`);
|
|
15609
16343
|
process.exit(1);
|
|
15610
16344
|
}
|
|
15611
16345
|
const result = await runRegistryRemoveCore(name, {
|
|
@@ -15613,26 +16347,30 @@ function registerRegistryCommand(program2) {
|
|
|
15613
16347
|
isLocked: () => config.lockRegistries ?? false,
|
|
15614
16348
|
removeRegistry: async (regName) => {
|
|
15615
16349
|
delete config.registries[regName];
|
|
15616
|
-
await writeOcxConfig(
|
|
16350
|
+
await writeOcxConfig(target.configDir, config, target.configPath);
|
|
15617
16351
|
}
|
|
15618
16352
|
});
|
|
15619
16353
|
if (options2.json) {
|
|
15620
16354
|
outputJson({ success: true, data: result });
|
|
15621
16355
|
} else if (!options2.quiet) {
|
|
15622
|
-
logger.success(`Removed registry: ${result.removed}`);
|
|
16356
|
+
logger.success(`Removed registry from ${target.targetLabel}: ${result.removed}`);
|
|
15623
16357
|
}
|
|
15624
16358
|
} catch (error) {
|
|
15625
16359
|
handleError(error, { json: options2.json });
|
|
15626
16360
|
}
|
|
15627
16361
|
});
|
|
15628
16362
|
const listCmd = registry.command("list").description("List configured registries");
|
|
16363
|
+
addGlobalOption(listCmd);
|
|
16364
|
+
addProfileOption(listCmd);
|
|
15629
16365
|
addCommonOptions(listCmd);
|
|
15630
|
-
listCmd.action(async (options2) => {
|
|
16366
|
+
listCmd.action(async (options2, command) => {
|
|
15631
16367
|
try {
|
|
15632
16368
|
const cwd = options2.cwd ?? process.cwd();
|
|
15633
|
-
const
|
|
16369
|
+
const target = await resolveRegistryTarget(options2, command, cwd);
|
|
16370
|
+
const config = await readOcxConfig(target.configDir);
|
|
15634
16371
|
if (!config) {
|
|
15635
|
-
|
|
16372
|
+
const initHint = target.scope === "global" ? "Run 'ocx init --global' first." : target.scope === "profile" ? `Run 'ocx profile config ${options2.profile}' to create it.` : "Run 'ocx init' first.";
|
|
16373
|
+
logger.warn(`${target.targetLabel} not found. ${initHint}`);
|
|
15636
16374
|
return;
|
|
15637
16375
|
}
|
|
15638
16376
|
const result = runRegistryListCore({
|
|
@@ -15645,7 +16383,8 @@ function registerRegistryCommand(program2) {
|
|
|
15645
16383
|
if (result.registries.length === 0) {
|
|
15646
16384
|
logger.info("No registries configured.");
|
|
15647
16385
|
} else {
|
|
15648
|
-
|
|
16386
|
+
const scopeLabel = target.scope === "global" ? " (global)" : target.scope === "profile" ? ` (profile '${options2.profile}')` : "";
|
|
16387
|
+
logger.info(`Configured registries${scopeLabel}${result.locked ? kleur_default.yellow(" (locked)") : ""}:`);
|
|
15649
16388
|
for (const reg of result.registries) {
|
|
15650
16389
|
console.log(` ${kleur_default.cyan(reg.name)}: ${reg.url} ${kleur_default.dim(`(${reg.version})`)}`);
|
|
15651
16390
|
}
|
|
@@ -15780,7 +16519,7 @@ async function runSearchCore(query, options2, provider) {
|
|
|
15780
16519
|
|
|
15781
16520
|
// src/self-update/version-provider.ts
|
|
15782
16521
|
class BuildTimeVersionProvider {
|
|
15783
|
-
version = "1.4.
|
|
16522
|
+
version = "1.4.2";
|
|
15784
16523
|
}
|
|
15785
16524
|
var defaultVersionProvider = new BuildTimeVersionProvider;
|
|
15786
16525
|
|
|
@@ -15893,7 +16632,7 @@ function getExecutablePath() {
|
|
|
15893
16632
|
}
|
|
15894
16633
|
|
|
15895
16634
|
// src/self-update/download.ts
|
|
15896
|
-
import { chmodSync, existsSync as
|
|
16635
|
+
import { chmodSync, existsSync as existsSync11, renameSync as renameSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
15897
16636
|
var GITHUB_REPO2 = "kdcokenny/ocx";
|
|
15898
16637
|
var DEFAULT_DOWNLOAD_BASE_URL = `https://github.com/${GITHUB_REPO2}/releases/download`;
|
|
15899
16638
|
var PLATFORM_MAP = {
|
|
@@ -15971,7 +16710,7 @@ async function downloadToTemp(version) {
|
|
|
15971
16710
|
try {
|
|
15972
16711
|
chmodSync(tempPath, 493);
|
|
15973
16712
|
} catch (error) {
|
|
15974
|
-
if (
|
|
16713
|
+
if (existsSync11(tempPath)) {
|
|
15975
16714
|
unlinkSync2(tempPath);
|
|
15976
16715
|
}
|
|
15977
16716
|
throw new SelfUpdateError(`Failed to set permissions: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -15981,20 +16720,20 @@ async function downloadToTemp(version) {
|
|
|
15981
16720
|
function atomicReplace(tempPath, execPath) {
|
|
15982
16721
|
const backupPath = `${execPath}.backup`;
|
|
15983
16722
|
try {
|
|
15984
|
-
if (
|
|
16723
|
+
if (existsSync11(execPath)) {
|
|
15985
16724
|
renameSync2(execPath, backupPath);
|
|
15986
16725
|
}
|
|
15987
16726
|
renameSync2(tempPath, execPath);
|
|
15988
|
-
if (
|
|
16727
|
+
if (existsSync11(backupPath)) {
|
|
15989
16728
|
unlinkSync2(backupPath);
|
|
15990
16729
|
}
|
|
15991
16730
|
} catch (error) {
|
|
15992
|
-
if (
|
|
16731
|
+
if (existsSync11(backupPath) && !existsSync11(execPath)) {
|
|
15993
16732
|
try {
|
|
15994
16733
|
renameSync2(backupPath, execPath);
|
|
15995
16734
|
} catch {}
|
|
15996
16735
|
}
|
|
15997
|
-
if (
|
|
16736
|
+
if (existsSync11(tempPath)) {
|
|
15998
16737
|
try {
|
|
15999
16738
|
unlinkSync2(tempPath);
|
|
16000
16739
|
} catch {}
|
|
@@ -16003,7 +16742,7 @@ function atomicReplace(tempPath, execPath) {
|
|
|
16003
16742
|
}
|
|
16004
16743
|
}
|
|
16005
16744
|
function cleanupTempFile(tempPath) {
|
|
16006
|
-
if (
|
|
16745
|
+
if (existsSync11(tempPath)) {
|
|
16007
16746
|
try {
|
|
16008
16747
|
unlinkSync2(tempPath);
|
|
16009
16748
|
} catch {}
|
|
@@ -16025,7 +16764,7 @@ function notifyUpdated(from, to) {
|
|
|
16025
16764
|
}
|
|
16026
16765
|
|
|
16027
16766
|
// src/self-update/verify.ts
|
|
16028
|
-
import { createHash as
|
|
16767
|
+
import { createHash as createHash3 } from "crypto";
|
|
16029
16768
|
var GITHUB_REPO3 = "kdcokenny/ocx";
|
|
16030
16769
|
function parseSha256Sums(content2) {
|
|
16031
16770
|
const checksums = new Map;
|
|
@@ -16038,8 +16777,8 @@ function parseSha256Sums(content2) {
|
|
|
16038
16777
|
}
|
|
16039
16778
|
return checksums;
|
|
16040
16779
|
}
|
|
16041
|
-
function
|
|
16042
|
-
return
|
|
16780
|
+
function hashContent3(content2) {
|
|
16781
|
+
return createHash3("sha256").update(content2).digest("hex");
|
|
16043
16782
|
}
|
|
16044
16783
|
async function fetchChecksums(version) {
|
|
16045
16784
|
const url = `https://github.com/${GITHUB_REPO3}/releases/download/v${version}/SHA256SUMS.txt`;
|
|
@@ -16053,7 +16792,7 @@ async function fetchChecksums(version) {
|
|
|
16053
16792
|
async function verifyChecksum(filePath, expectedHash, filename) {
|
|
16054
16793
|
const file = Bun.file(filePath);
|
|
16055
16794
|
const content2 = await file.arrayBuffer();
|
|
16056
|
-
const actualHash =
|
|
16795
|
+
const actualHash = hashContent3(Buffer.from(content2));
|
|
16057
16796
|
if (actualHash !== expectedHash) {
|
|
16058
16797
|
throw new IntegrityError(filename, expectedHash, actualHash);
|
|
16059
16798
|
}
|
|
@@ -16178,10 +16917,10 @@ function registerSelfCommand(program2) {
|
|
|
16178
16917
|
}
|
|
16179
16918
|
|
|
16180
16919
|
// src/commands/update.ts
|
|
16181
|
-
import { createHash as
|
|
16182
|
-
import { existsSync as
|
|
16183
|
-
import { mkdir as
|
|
16184
|
-
import { dirname as
|
|
16920
|
+
import { createHash as createHash4 } from "crypto";
|
|
16921
|
+
import { existsSync as existsSync12 } from "fs";
|
|
16922
|
+
import { mkdir as mkdir9, writeFile as writeFile4 } from "fs/promises";
|
|
16923
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
16185
16924
|
function registerUpdateCommand(program2) {
|
|
16186
16925
|
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
16926
|
try {
|
|
@@ -16265,7 +17004,7 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
|
|
|
16265
17004
|
const content2 = await fetchFileContent(regConfig.url, componentName, file.path);
|
|
16266
17005
|
files.push({ path: file.path, content: Buffer.from(content2) });
|
|
16267
17006
|
}
|
|
16268
|
-
const newHash = await
|
|
17007
|
+
const newHash = await hashBundle3(files);
|
|
16269
17008
|
if (newHash === lockEntry.hash) {
|
|
16270
17009
|
results.push({
|
|
16271
17010
|
qualifiedName,
|
|
@@ -16319,12 +17058,12 @@ Version cannot be empty. Use 'kdco/agents@1.2.0' or omit the version for latest.
|
|
|
16319
17058
|
const fileObj = update.component.files.find((f) => f.path === file.path);
|
|
16320
17059
|
if (!fileObj)
|
|
16321
17060
|
continue;
|
|
16322
|
-
const targetPath =
|
|
16323
|
-
const targetDir =
|
|
16324
|
-
if (!
|
|
16325
|
-
await
|
|
17061
|
+
const targetPath = join10(provider.cwd, fileObj.target);
|
|
17062
|
+
const targetDir = dirname6(targetPath);
|
|
17063
|
+
if (!existsSync12(targetDir)) {
|
|
17064
|
+
await mkdir9(targetDir, { recursive: true });
|
|
16326
17065
|
}
|
|
16327
|
-
await
|
|
17066
|
+
await writeFile4(targetPath, file.content);
|
|
16328
17067
|
if (options2.verbose) {
|
|
16329
17068
|
logger.info(` \u2713 Updated ${fileObj.target}`);
|
|
16330
17069
|
}
|
|
@@ -16443,17 +17182,17 @@ function outputDryRun(results, options2) {
|
|
|
16443
17182
|
}
|
|
16444
17183
|
}
|
|
16445
17184
|
}
|
|
16446
|
-
async function
|
|
16447
|
-
return
|
|
17185
|
+
async function hashContent4(content2) {
|
|
17186
|
+
return createHash4("sha256").update(content2).digest("hex");
|
|
16448
17187
|
}
|
|
16449
|
-
async function
|
|
17188
|
+
async function hashBundle3(files) {
|
|
16450
17189
|
const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
|
|
16451
17190
|
const manifestParts = [];
|
|
16452
17191
|
for (const file of sorted) {
|
|
16453
|
-
const hash = await
|
|
17192
|
+
const hash = await hashContent4(file.content);
|
|
16454
17193
|
manifestParts.push(`${file.path}:${hash}`);
|
|
16455
17194
|
}
|
|
16456
|
-
return
|
|
17195
|
+
return hashContent4(manifestParts.join(`
|
|
16457
17196
|
`));
|
|
16458
17197
|
}
|
|
16459
17198
|
|
|
@@ -16485,7 +17224,7 @@ function registerUpdateCheckHook(program2) {
|
|
|
16485
17224
|
});
|
|
16486
17225
|
}
|
|
16487
17226
|
// src/index.ts
|
|
16488
|
-
var version = "1.4.
|
|
17227
|
+
var version = "1.4.2";
|
|
16489
17228
|
async function main2() {
|
|
16490
17229
|
const program2 = new Command().name("ocx").description("OpenCode Extensions - Install agents, skills, plugins, and commands").version(version);
|
|
16491
17230
|
registerInitCommand(program2);
|
|
@@ -16517,4 +17256,4 @@ export {
|
|
|
16517
17256
|
buildRegistry
|
|
16518
17257
|
};
|
|
16519
17258
|
|
|
16520
|
-
//# debugId=
|
|
17259
|
+
//# debugId=52AD8D268EBA9E8864756E2164756E21
|