Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,14 +372,23 @@ plugins/ # DEPRECATED: legacy plugins still work with warning

### Installing a Package

The Wheels 4.1 CLI will ship `wheels packages install <name>` which resolves names against the `wheels-dev/wheels-packages` registry. Until then, interim install is a manual clone:
Use the `wheels packages` CLI. Resolves names against the `wheels-dev/wheels-packages` registry, verifies sha256, extracts to `vendor/<name>/`.

```bash
gh repo clone wheels-dev/wheels-sentry vendor/wheels-sentry # install
rm -rf vendor/wheels-sentry # remove
```

Restart or reload the app after install.
wheels packages list # browse the registry
wheels packages search <query> # name/description/tag match
wheels packages show <name> # detail page
wheels packages install <name> # latest compat version
wheels packages install <name>@<version> # pin
wheels packages install <name> --force # overwrite an existing vendor/<name>
wheels packages update <name> --yes # explicit update
wheels packages update --all --yes # update every installed package
wheels packages remove <name> # delete vendor/<name>
wheels packages registry refresh # bust the 24h cache
wheels packages registry info # show registry URL + cache state
```

Override the registry with `WHEELS_PACKAGES_REGISTRY=<org>/<repo>` (defaults to `wheels-dev/wheels-packages`). Restart or `wheels reload` after install.

### Error Isolation

Expand Down
118 changes: 118 additions & 0 deletions cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,124 @@ component extends="modules.BaseModule" {
return opts;
}

// ─────────────────────────────────────────────────
// packages — registry-backed package manager
// ─────────────────────────────────────────────────

/**
* hint: Install, update, and list packages from the wheels-packages registry
*
* Usage:
* wheels packages list [--tag=<tag>]
* wheels packages search <query>
* wheels packages show <name>
* wheels packages install <name>[@<version>] [--force]
* wheels packages update <name> --yes
* wheels packages update --all --yes
* wheels packages remove <name>
* wheels packages registry refresh
* wheels packages registry info
*/
public string function packages() {
var args = getArgs(arguments);
var opts = $packagesArgsToOptions(args);
var positional = $packagesStripFlags(args);
var sub = arrayLen(positional) >= 1 ? positional[1] : "list";

switch (sub) {
case "list":
var mainCli = new cli.lucli.services.packages.PackagesMainCli();
return mainCli.list(opts);
case "search":
if (arrayLen(positional) < 2) {
throw(message="search requires a query: wheels packages search <query>");
}
opts.query = positional[2];
var mainCli = new cli.lucli.services.packages.PackagesMainCli();
return mainCli.search(opts);
case "show":
if (arrayLen(positional) < 2) {
throw(message="show requires a name: wheels packages show <name>");
}
opts.name = positional[2];
var mainCli = new cli.lucli.services.packages.PackagesMainCli();
return mainCli.show(opts);
case "install":
if (arrayLen(positional) < 2) {
throw(message="install requires a name: wheels packages install <name>[@<version>]");
}
opts.target = positional[2];
var mainCli = new cli.lucli.services.packages.PackagesMainCli();
return mainCli.install(opts);
case "update":
opts.target = arrayLen(positional) >= 2 ? positional[2] : "";
var mainCli = new cli.lucli.services.packages.PackagesMainCli();
return mainCli.update(opts);
case "remove":
if (arrayLen(positional) < 2) {
throw(message="remove requires a name: wheels packages remove <name>");
}
opts.target = positional[2];
var mainCli = new cli.lucli.services.packages.PackagesMainCli();
return mainCli.remove(opts);
case "registry":
if (arrayLen(positional) < 2) {
throw(message="wheels packages registry requires a verb (refresh or info)");
}
var regVerb = positional[2];
if (!listFindNoCase("refresh,info", regVerb)) {
throw(message="Unknown wheels packages registry verb: #regVerb#");
}
var regCli = new cli.lucli.services.packages.PackagesRegistryCli();
return invoke(regCli, regVerb, [opts]);
default:
throw(message="Unknown packages subcommand: #sub#");
}
}

private struct function $packagesArgsToOptions(required array args) {
var opts = {};
var n = arrayLen(arguments.args);
var i = 1;
while (i <= n) {
var a = arguments.args[i];
if (a == "--all") {
opts.all = true;
} else if (a == "--yes") {
opts.yes = true;
} else if (a == "--force") {
opts.force = true;
} else if (left(a, 6) == "--tag=") {
opts.tag = mid(a, 7, 99999);
} else if (a == "--tag" && i < n) {
opts.tag = arguments.args[i+1];
i++;
}
i++;
}
return opts;
}

private array function $packagesStripFlags(required array args) {
var out = [];
var n = arrayLen(arguments.args);
var i = 1;
while (i <= n) {
var a = arguments.args[i];
if (left(a, 2) == "--") {
var booleans = "--all,--yes,--force";
if (!find("=", a) && !listFindNoCase(booleans, a) && i < n && left(arguments.args[i+1], 2) != "--") {
i++;
}
i++;
continue;
}
arrayAppend(out, a);
i++;
}
return out;
}

private array function $deployStripFlags(required array args) {
var out = [];
var n = arrayLen(arguments.args);
Expand Down
63 changes: 63 additions & 0 deletions cli/lucli/services/packages/HttpClient.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Thin HTTP GET wrapper. Exists so Registry can be tested offline with
* a fake that returns canned responses keyed by URL.
*
* Uses cfhttp (script syntax) for cross-engine portability — `new http()`
* is Lucee-specific and the framework test matrix also runs these specs
* against Adobe CF.
*/
component {

public HttpClient function init(numeric timeoutSeconds = 30) {
variables.timeout = arguments.timeoutSeconds;
return this;
}

/**
* @return struct { status: numeric, body: string }
*/
public struct function get(required string url, struct headers = {}) {
cfhttp(
url = arguments.url,
method = "GET",
timeout = variables.timeout,
result = "local.result"
) {
for (local.name in arguments.headers) {
cfhttpparam(type = "header", name = local.name, value = arguments.headers[local.name]);
}
// GitHub's API prefers an explicit User-Agent on unauth requests.
cfhttpparam(type = "header", name = "User-Agent", value = "wheels-cli");
}
return {
status: Val(ListFirst(local.result.statusCode, " ")),
body: local.result.fileContent ?: ""
};
}

/**
* Downloads bytes to disk. Separate from get() because tarballs are
* large binary and `fileContent` as a string is the wrong abstraction.
* Returns the destination path on success, throws on non-200.
*/
public string function download(required string url, required string destPath) {
cfhttp(
url = arguments.url,
method = "GET",
timeout = variables.timeout,
getAsBinary = "yes",
result = "local.result"
) {
cfhttpparam(type = "header", name = "User-Agent", value = "wheels-cli");
}
local.status = Val(ListFirst(local.result.statusCode, " "));
if (local.status != 200) {
Throw(
type = "Wheels.Packages.DownloadFailed",
message = "Download failed: HTTP #local.status# for #arguments.url#"
);
}
FileWrite(arguments.destPath, local.result.fileContent);
return arguments.destPath;
}
}
Loading
Loading