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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ For an agent or script, provide credentials through environment variables or a c
| Install the Skill | `sqlx skill install --target codex`, `--target claude`, `--target dsh` or `--target pi` |
| Install to another skill directory | `sqlx skill install --path /path/to/skills/sqlx` |
| Inspect/update managed Skills | `sqlx skill status`, `sqlx skill update` |
| Stop managing a Skill installation | `sqlx skill remove --path /path/to/skills/sqlx` (files are kept) |
| Help/version | `sqlx --help`, `sqlx --version` |

`--id` and `--datasource` accept a stable datasource UUID or its unique name. Supported database type names are `mysql`, `postgresql` (`postgres`/`pgsql`), `oracle`, and `sqlserver` (`mssql`). Oracle requires `--service`. TLS defaults to certificate verification; `--tls disable` is available for explicitly unencrypted connections. The first authentication profile is username/password.
Expand Down
6 changes: 6 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,11 @@ enum SkillCommand {
},
Update,
Status,
/// Stop managing a Skill installation; its files are left in place.
Remove {
#[arg(long)]
path: PathBuf,
},
}
fn main() {
let cli = Cli::parse();
Expand Down Expand Up @@ -478,6 +483,7 @@ fn run(cli: Cli) -> Result<bool> {
}
SkillCommand::Update => skill::update(&manager)?,
SkillCommand::Status => skill::status(&manager)?,
SkillCommand::Remove { path } => skill::remove(&manager, path)?,
};
print(value);
}
Expand Down
131 changes: 130 additions & 1 deletion crates/cli/src/skill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,61 @@ pub fn target_path(target: Option<String>, path: Option<PathBuf>) -> Result<Path
}
pub fn status(manager: &Components) -> Result<Value> {
Ok(
json!({"installations":records(manager)?.into_iter().map(|i|json!({"path":i.path,"version":i.version,"intact":files(&i.path).is_ok_and(|f|f==i.files)})).collect::<Vec<_>>()}),
json!({"installations":records(manager)?.into_iter().map(|i|{let missing=!i.path.exists();json!({"path":i.path,"version":i.version,"missing":missing,"intact":!missing&&files(&i.path).is_ok_and(|f|f==i.files)})}).collect::<Vec<_>>()}),
)
}
/// Canonicalize the nearest existing ancestor and re-append the missing tail, so a
/// record stays addressable after its directory was deleted (for example under /tmp).
fn resolved_path(path: &Path) -> PathBuf {
if let Ok(canonical) = path.canonicalize() {
return canonical;
}
let mut missing = Vec::new();
let mut current = path;
loop {
match current.canonicalize() {
Ok(base) => {
let mut resolved = base;
for part in missing.iter().rev() {
resolved.push(part);
}
return resolved;
}
Err(_) => match (current.file_name(), current.parent()) {
(Some(name), Some(parent)) => {
missing.push(name.to_os_string());
current = parent;
}
_ => return path.to_path_buf(),
},
}
}
}
/// Stop managing a recorded Skill installation. Its files are never deleted here.
pub fn remove(manager: &Components, path: PathBuf) -> Result<Value> {
let lock = open_private(&manager.root.join("skill-installs.lock"))?;
lock.lock_exclusive()?;
let mut installs = records(manager)?;
let absolute = if path.is_absolute() {
path
} else {
std::env::current_dir()?.join(path)
};
let target = resolved_path(&absolute);
let before = installs.len();
installs.retain(|i| i.path != target && i.path != absolute);
if installs.len() == before {
bail!(
"no SQLX-managed Skill is recorded at {}; run `sqlx skill status` to list them",
absolute.display()
);
}
atomic_write(
&manager.root.join("skill-installs.json"),
&serde_json::to_vec_pretty(&installs)?,
)?;
Ok(json!({"removed":true,"path":target,"directory_exists":target.exists()}))
}
pub fn install(manager: &Components, path: PathBuf) -> Result<Value> {
let manifest = manager.manifest(false)?;
let asset = manager.asset(&manifest, "skill", "any")?;
Expand Down Expand Up @@ -176,4 +228,81 @@ mod tests {
.to_string();
assert!(error.contains("codex, claude, dsh, pi"), "{error}");
}
#[test]
fn remove_forgets_one_record_and_keeps_its_files() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_path_buf();
let manager = Components::new(root.clone(), "unused".into());
let recorded = root.join("skills/sqlx");
let other = root.join("other/sqlx");
fs::create_dir_all(&recorded).unwrap();
fs::create_dir_all(&other).unwrap();
let entry = |path: PathBuf| Installation {
path,
version: "0.1.6".into(),
files: BTreeMap::new(),
};
atomic_write(
&root.join("skill-installs.json"),
&serde_json::to_vec_pretty(&[entry(recorded.clone()), entry(other.clone())]).unwrap(),
)
.unwrap();
let value = remove(&manager, recorded.clone()).unwrap();
assert_eq!(value["removed"], json!(true));
assert_eq!(value["directory_exists"], json!(true));
assert!(recorded.exists(), "removal must not delete files");
let left = records(&manager).unwrap();
assert_eq!(left.len(), 1);
assert_eq!(left[0].path, other);
let error = remove(&manager, recorded).unwrap_err().to_string();
assert!(
error.contains("no SQLX-managed Skill is recorded"),
"{error}"
);
}
#[test]
fn remove_resolves_a_path_whose_directory_is_gone() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_path_buf();
let manager = Components::new(root.clone(), "unused".into());
let recorded = root.join("gone/sqlx");
fs::create_dir_all(&recorded).unwrap();
// The installer records the canonical path; remove must still find it after deletion.
let canonical = recorded.canonicalize().unwrap();
atomic_write(
&root.join("skill-installs.json"),
&serde_json::to_vec_pretty(&[Installation {
path: canonical,
version: "0.1.6".into(),
files: BTreeMap::new(),
}])
.unwrap(),
)
.unwrap();
fs::remove_dir_all(&recorded).unwrap();
let value = remove(&manager, recorded).unwrap();
assert_eq!(value["removed"], json!(true));
assert_eq!(value["directory_exists"], json!(false));
assert!(records(&manager).unwrap().is_empty());
}
#[test]
fn status_reports_a_missing_installation() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_path_buf();
let manager = Components::new(root.clone(), "unused".into());
let gone = root.join("gone/sqlx");
atomic_write(
&root.join("skill-installs.json"),
&serde_json::to_vec_pretty(&[Installation {
path: gone.clone(),
version: "0.1.6".into(),
files: BTreeMap::new(),
}])
.unwrap(),
)
.unwrap();
let value = status(&manager).unwrap();
assert_eq!(value["installations"][0]["missing"], json!(true));
assert_eq!(value["installations"][0]["intact"], json!(false));
}
}
2 changes: 1 addition & 1 deletion skills/sqlx/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Local page URLs can be reopened without an authorization deadline. Run `sqlx ui

When the user wants to inspect data visually, append `--view` to the already prepared SQL execution command. Apply the approval gate above before starting the query. It returns a local result URL and starts the query once in the local service. The user does not need to paste or rerun the SQL. Browser reload and pagination read the same cached execution. In SQLX 0.1.3, the page's **Refresh** action explicitly reruns all original SQL statements in order; apply the approval gate again before a state-changing or unknown refresh. The optional refresh interval does the same; do not enable it unless the user explicitly approves repeated execution and its side effects. SQL is not classified or rewritten, so writes in the original batch run again too. UI results are retained locally for 24 hours; do not automatically rerun expired or interrupted queries. `--no-open` returns a link without launching a browser. These pages open on the machine running SQLX. See [local pages](references/local-ui.md) for credential editing, UI plugin installation and selection, lifecycle, and limitations.

Skill installation and updates use `sqlx skill install --target codex`, `--target claude`, `--target dsh`, `--target pi`, or `--path <skill-directory>`, followed by the target agent's discovery/reload mechanism. Codex and dsh share `~/.agents/skills`; Pi uses `~/.pi/agent/skills`. `sqlx skill status` and `sqlx skill update` operate on SQLX-managed installations and preserve locally modified skill files.
Skill installation and updates use `sqlx skill install --target codex`, `--target claude`, `--target dsh`, `--target pi`, or `--path <skill-directory>`, followed by the target agent's discovery/reload mechanism. Codex and dsh share `~/.agents/skills`; Pi uses `~/.pi/agent/skills`. `sqlx skill status` and `sqlx skill update` operate on SQLX-managed installations and preserve locally modified skill files. `status` marks a record whose directory no longer exists with `missing: true`, and `sqlx skill remove --path <skill-directory>` stops managing that installation without deleting any files.

## Check and install CLI updates

Expand Down
Loading