@ottermind/sqlx 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,178 @@
1
+ # License
2
+
3
+ Chat2DB Rust is licensed under a modified version of the Apache License 2.0,
4
+ with the additional conditions below.
5
+
6
+ The Chat2DB Community source used by Chat2DB Rust remains subject to the
7
+ license included with that source. Third-party components remain subject to
8
+ their own license terms and notices.
9
+
10
+ For package metadata, these terms may be identified as
11
+ `LicenseRef-Chat2DB`. That identifier refers only to this root `LICENSE` file
12
+ and does not create a separate license.
13
+
14
+ ## 1. Permitted Use
15
+
16
+ Subject to this License, you may use, copy, and modify the Software for:
17
+
18
+ a. personal self-use, or self-hosted use by an educational, academic research,
19
+ or non-profit organization; and
20
+
21
+ b. Internal Use by you and Your Organization, including internal use of the
22
+ desktop application, Web interface, Docker image, HTTP API, CLI, MCP,
23
+ plugins, and reusable modules.
24
+
25
+ You may publish and redistribute the Software or your modifications in Source
26
+ form under this License, provided that you keep this License and all applicable
27
+ copyright, attribution, and third-party notices. This permission does not allow
28
+ you to provide an External Product or Service, Managed Delivery, Embedded
29
+ Product Use, white-label delivery, OEM delivery, or Object-form distribution
30
+ to an Independent External Party without written commercial authorization.
31
+
32
+ ## 2. Internal Processing and Results Delivery
33
+
34
+ Internal Use includes use by your Authorized Personnel to process data owned
35
+ or provided by an Independent External Party and to deliver reports, analyses,
36
+ migration results, or other non-interactive outputs to that party, provided
37
+ that the party does not receive credentials, interfaces, configurations,
38
+ controls, repeatable automated access, or any other ability to operate,
39
+ configure, invoke, integrate, or direct the Software or its Community Core
40
+ Capabilities.
41
+
42
+ An Independent External Party providing business requirements, data, or
43
+ acceptance criteria does not by itself make the use external. However, shared
44
+ accounts, human intermediaries, proxy interfaces, prompts, agents, workflows,
45
+ plugins, scheduled requests, or similar arrangements do not qualify for this
46
+ exception when they give the party repeatable or practical control over the
47
+ Software or its Community Core Capabilities.
48
+
49
+ ## 3. Commercial Authorization Required
50
+
51
+ Unless Chat2DB has expressly authorized you in writing, you may not:
52
+
53
+ a. provide an External Product or Service;
54
+
55
+ b. perform Managed Delivery for an Independent External Party;
56
+
57
+ c. except for the Source-form redistribution expressly permitted by Section 1,
58
+ distribute installers, binaries, archives, container images, deployment
59
+ charts, patches, configurations, or other materials that enable an
60
+ Independent External Party to deploy or use the Software in Object form;
61
+
62
+ d. perform Embedded Product Use;
63
+
64
+ e. provide white-label or OEM versions of the Software or a derivative work;
65
+ or
66
+
67
+ f. remove, obscure, or modify Chat2DB logos, copyright information, licensing
68
+ notices, or attribution displayed by the official frontend or included in
69
+ an official distribution.
70
+
71
+ These conditions apply whether the offering is paid or free, whether access is
72
+ direct or indirect, whether accounts exist, whether there is one tenant or many
73
+ tenants, and whether the Software runs in a shared instance, a dedicated
74
+ instance, a customer-owned cloud account, or any other deployment topology.
75
+ The identity of the payer, account owner, infrastructure owner, distributor,
76
+ or operator does not change the result. The relevant question is whether an
77
+ Independent External Party receives the ability to use Community Core
78
+ Capabilities.
79
+
80
+ For written commercial authorization, contact Chat2DB through
81
+ https://chat2db.ai.
82
+
83
+ ## 4. Definitions
84
+
85
+ "Chat2DB" means 爱獭科技(杭州)有限公司, the producer of the Software.
86
+
87
+ "Software" means the Chat2DB Rust software made available under this License
88
+ in Source or Object form, including its official desktop, Web, HTTP API, CLI,
89
+ MCP, compatibility engine, plugins, and reusable module distributions.
90
+
91
+ "Source" and "Object" have the meanings given in the Apache License 2.0.
92
+
93
+ "You" means the individual or legal entity exercising permissions under this
94
+ License.
95
+
96
+ "Your Organization" means you and any legal entities that control, are
97
+ controlled by, or are under common control with you. "Control" means direct or
98
+ indirect ownership of more than fifty percent of the voting interests, or the
99
+ power to direct the management and policies of an entity by ownership,
100
+ contract, or otherwise.
101
+
102
+ "Authorized Personnel" means employees and individual contractors acting
103
+ solely on behalf of Your Organization, under obligations that protect the
104
+ Software and prevent its use for any other person or organization. Customers,
105
+ vendors, channel partners, distributors, and independent service providers are
106
+ not Authorized Personnel merely because they have a commercial relationship
107
+ with you.
108
+
109
+ "Internal Use" means use of the Software solely by Authorized Personnel and
110
+ internal systems for the internal operations of Your Organization, where no
111
+ Independent External Party receives Community Core Capabilities.
112
+
113
+ "Independent External Party" means any individual or organization outside
114
+ Your Organization that is not Authorized Personnel.
115
+
116
+ "Community Core Capabilities" means substantive database functionality
117
+ provided by the Software, including database connection and credential
118
+ management, metadata browsing, SQL, DDL, or DML execution, data editing,
119
+ import or export, tasks and history, dashboards and charts, AI features, CLI,
120
+ MCP, frontend interaction, HTTP APIs, plugins, and reusable API, Core, or SPI
121
+ modules.
122
+
123
+ "External Product or Service" means any product or service through which an
124
+ Independent External Party can directly or indirectly operate, configure,
125
+ invoke, integrate, automate, or otherwise use Community Core Capabilities,
126
+ including through a Web UI, desktop application, HTTP API, CLI, MCP, embedded
127
+ module, prompt, agent, workflow, proxy, or other interface.
128
+
129
+ "Managed Delivery" means deploying, configuring, customizing, hosting,
130
+ operating, maintaining, upgrading, or continuously administering the Software
131
+ or a derivative work for an Independent External Party in a way that gives
132
+ that party Community Core Capabilities.
133
+
134
+ "Embedded Product Use" means incorporating the Software, its frontend, HTTP
135
+ API, CLI, MCP, plugins, reusable modules, or a derivative implementation into
136
+ a product made available to an Independent External Party, where that product
137
+ provides substantive Community Core Capabilities.
138
+
139
+ ## 5. Branding and Notices
140
+
141
+ This License does not grant permission to use Chat2DB trade names, trademarks,
142
+ service marks, or product names except as required to describe the origin of
143
+ the Software and reproduce required notices. All uses of Chat2DB marks remain
144
+ subject to applicable trademark law and any separately published trademark
145
+ policy.
146
+
147
+ If you modify the Software, you must include prominent notices stating that
148
+ you changed it. You must retain all applicable copyright, patent, trademark,
149
+ licensing, and attribution notices from the Software.
150
+
151
+ ## 6. Contributions
152
+
153
+ Unless you explicitly state otherwise in writing before submission, any
154
+ contribution intentionally submitted for inclusion in Chat2DB Rust is provided
155
+ under this License. By submitting a contribution, you also agree that Chat2DB
156
+ may use the contribution for commercial purposes and may include it in future
157
+ Chat2DB releases made available under different license terms.
158
+
159
+ ## 7. Third-Party Software
160
+
161
+ Third-party components included with the Software remain subject to their own
162
+ license terms and notices. This License does not replace, restrict, or modify
163
+ those third-party terms. Where a file or component contains a separate license
164
+ or copyright notice, that separate notice governs that file or component.
165
+
166
+ ## 8. Apache License 2.0 Terms
167
+
168
+ Except for the additional conditions stated in this License, all other rights
169
+ and restrictions follow the Apache License 2.0, available at
170
+ https://www.apache.org/licenses/LICENSE-2.0. If these additional conditions
171
+ conflict with the Apache License 2.0 for Chat2DB Rust, these additional
172
+ conditions control.
173
+
174
+ The Apache License 2.0 provisions concerning patent grants, patent litigation,
175
+ notices, disclaimer of warranty, limitation of liability, and acceptance of
176
+ additional liability are incorporated into this License.
177
+
178
+ Copyright 2026 爱獭科技(杭州)有限公司.
package/NOTICE ADDED
@@ -0,0 +1,14 @@
1
+ SQLX
2
+ Copyright 2026 爱獭科技(杭州)有限公司 / OtterMind.
3
+
4
+ Database connection, result conversion, and JDBC loading code is adapted from
5
+ OtterMind/Chat2DB-Rust, commit e17ee8be373407e193e7e12da0dcd3b5b4b5550b.
6
+ These adaptations replace product-specific storage and session integrations
7
+ with standalone worker processes and complete streamed SQL results.
8
+ The retained LICENSE governs the derived code. Third-party dependencies keep
9
+ their own licenses. SQLX is a public source repository; its license contains
10
+ additional conditions beyond the standard Apache License 2.0.
11
+
12
+ Result toolbar sizing is adapted from Chat2DB Community (commit 777b06099):
13
+ chat2db-community-client/src/blocks/SearchResult/components/ResultSetToolbar.
14
+ The original license and copyright conditions remain applicable.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # @ottermind/sqlx
2
+
3
+ Install or update the [SQLX](https://github.com/OtterMind/sqlx) database CLI and
4
+ its agent Skill with one command.
5
+
6
+ ```bash
7
+ npx -y @ottermind/sqlx@latest
8
+ ```
9
+
10
+ The command installs or updates the native CLI, initializes the local SQLX data
11
+ directory when it does not exist yet, and installs the Skill. Node.js is only
12
+ needed to run the installer: the CLI itself has no Node.js runtime dependency.
13
+
14
+ This package contains no binaries. It downloads the CLI and the Skill from the
15
+ GitHub Release that matches the version of this package.
16
+
17
+ ## Requirements
18
+
19
+ - Node.js 22 or newer (`npx` is part of npm).
20
+ - Write access to the CLI directory and to the Skill directory.
21
+ - Access to the npm Registry and to GitHub Releases.
22
+
23
+ Windows ARM64 is not a release target.
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ # Skill in ./sqlx, CLI in the official user-level location
29
+ npx -y @ottermind/sqlx@latest
30
+
31
+ # Skill in the Codex user-level Skill directory
32
+ npx -y @ottermind/sqlx@latest --target codex
33
+
34
+ # Skill in the Claude Code user-level Skill directory
35
+ npx -y @ottermind/sqlx@latest --target claude
36
+
37
+ # Skill in an explicit directory
38
+ npx -y @ottermind/sqlx@latest --target ./tools/sqlx
39
+
40
+ npx -y @ottermind/sqlx@latest --help
41
+ ```
42
+
43
+ | `--target` | Skill directory |
44
+ | ------------------- | ---------------------------------- |
45
+ | not given | `./sqlx` in the current directory |
46
+ | `codex` | `~/.agents/skills/sqlx` |
47
+ | `claude` | `~/.claude/skills/sqlx` |
48
+ | any other value | that path, used as the directory |
49
+
50
+ Relative paths are resolved against the directory the command runs from, not
51
+ against a temporary download directory.
52
+
53
+ ## Where things are installed
54
+
55
+ | Item | Location |
56
+ | ---------- | ----------------------------------------------------------- |
57
+ | CLI | `~/.local/bin/sqlx` (macOS, Linux) |
58
+ | CLI | `%LOCALAPPDATA%\Programs\SQLX\sqlx.exe` (Windows) |
59
+ | Skill | the `--target` directory |
60
+ | Local data | `~/.sqlx`, created by `sqlx init` when missing |
61
+
62
+ The CLI is reused when it is already at the target version, updated when it is
63
+ older and supports self-updates, and left untouched when it is newer: this
64
+ installer never downgrades a working CLI and never replaces an executable it
65
+ does not recognize as `OtterMind/sqlx`.
66
+
67
+ The Skill is installed by the verified CLI itself, so it shares the same
68
+ `skill-installs.json` record, integrity data and local-modification protection
69
+ as `sqlx skill install`. The installer never copies Skill files on its own, so
70
+ there is only one update rule.
71
+
72
+ If the CLI installs successfully but the Skill step fails, the command exits
73
+ non-zero, keeps the working CLI and reports the Skill failure separately.
74
+
75
+ ## Environment
76
+
77
+ | Variable | Meaning |
78
+ | ------------------- | ---------------------------------------------------------- |
79
+ | `SQLX_INSTALL_DIR` | CLI directory instead of the official location |
80
+ | `SQLX_RELEASE_BASE` | Release download base instead of `github.com` |
81
+ | `SQLX_DATA_DIR` | SQLX data directory instead of `~/.sqlx` |
82
+ | `SQLX_VERSION` | Release to install instead of this package version |
83
+
84
+ ## Verification
85
+
86
+ Before anything is written, the installer checks that the release metadata, the
87
+ `SHA256SUMS` file and the downloaded archive agree:
88
+
89
+ 1. `SHA256SUMS` is downloaded and `manifest.json` is verified against it.
90
+ 2. The CLI archive is verified against the SHA-256 in the manifest.
91
+ 3. The archive is extracted with a ZIP reader that rejects absolute paths, `..`
92
+ traversal, backslashes and symbolic links.
93
+ 4. The extracted binary is executed once as `--version` and must report
94
+ `OtterMind/sqlx` at the expected version.
95
+ 5. The binary is staged inside the CLI directory and renamed into place, so a
96
+ failure cannot leave a half-written executable.
97
+
98
+ ## This package does not
99
+
100
+ - run `preinstall`, `install` or `postinstall` scripts;
101
+ - write to the `package.json`, lockfile or `node_modules` of the calling
102
+ project;
103
+ - download database workers, drivers or a JRE — the CLI fetches those when a
104
+ connection actually needs them;
105
+ - modify `PATH` or shell profiles; it reports the directory to add when the CLI
106
+ is not reachable.
107
+
108
+ ## Troubleshooting
109
+
110
+ - **`SQLX installation failed (cli)`** — the CLI directory is not writable, or
111
+ it already contains a `sqlx` that is not an OtterMind build. Set
112
+ `SQLX_INSTALL_DIR` to a directory you own.
113
+ - **`SQLX installation failed (skill)`** — the Skill destination already exists
114
+ and is not managed by SQLX, or it has local changes. The CLI stays installed;
115
+ fix the destination and run the command again.
116
+ - **`SQLX installation failed (download)`** — the GitHub Release for this
117
+ package version is not reachable. Check the version exists before retrying.
118
+
119
+ No Node.js? Use the Shell or PowerShell installer described in the
120
+ [SQLX README](https://github.com/OtterMind/sqlx#readme).
121
+
122
+ ## License
123
+
124
+ See `LICENSE` and `NOTICE`. For package metadata these terms are identified as
125
+ `LicenseRef-Chat2DB`.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { run } from "../src/main.mjs";
3
+
4
+ process.exitCode = await run(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@ottermind/sqlx",
3
+ "version": "0.1.5",
4
+ "description": "Install or update the SQLX database CLI and its agent Skill.",
5
+ "license": "LicenseRef-Chat2DB",
6
+ "homepage": "https://github.com/OtterMind/sqlx#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/OtterMind/sqlx.git",
10
+ "directory": "packages/npm-installer"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/OtterMind/sqlx/issues"
14
+ },
15
+ "type": "module",
16
+ "bin": {
17
+ "sqlx-install": "bin/sqlx-install.mjs"
18
+ },
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "src",
25
+ "README.md",
26
+ "LICENSE",
27
+ "NOTICE"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/"
32
+ },
33
+ "scripts": {
34
+ "test": "node --test test/unit.test.mjs test/e2e.test.mjs"
35
+ }
36
+ }
package/src/args.mjs ADDED
@@ -0,0 +1,66 @@
1
+ import { UsageError } from "./errors.mjs";
2
+
3
+ export const HELP = `Install or update the SQLX CLI and its agent Skill.
4
+
5
+ Usage:
6
+ npx -y @ottermind/sqlx@latest [--target <codex|claude|path>]
7
+
8
+ Options:
9
+ --target <codex|claude|path> Skill destination. Without it the Skill is
10
+ installed to ./sqlx in the directory the command
11
+ runs from. "codex" and "claude" select the
12
+ user-level Skill directory of that agent; any
13
+ other value is used as the Skill directory.
14
+ -h, --help Show this help and exit.
15
+
16
+ The CLI is installed to its official user-level location:
17
+ macOS, Linux ~/.local/bin/sqlx
18
+ Windows %LOCALAPPDATA%\\Programs\\SQLX\\sqlx.exe
19
+
20
+ This package never runs an install script, never downloads inside a project and
21
+ never edits the package.json, lockfile or node_modules of the calling project.
22
+
23
+ Environment:
24
+ SQLX_INSTALL_DIR CLI directory instead of the official location.
25
+ SQLX_RELEASE_BASE Release download base instead of github.com.
26
+ SQLX_DATA_DIR SQLX data directory instead of ~/.sqlx.
27
+ SQLX_VERSION Release to install instead of this package version.
28
+
29
+ Examples:
30
+ npx -y @ottermind/sqlx@latest
31
+ npx -y @ottermind/sqlx@latest --target codex
32
+ npx -y @ottermind/sqlx@latest --target ./tools/sqlx
33
+ `;
34
+
35
+ export function parseArgs(argv) {
36
+ const options = { target: undefined, help: false };
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const argument = argv[index];
39
+ if (argument === "--help" || argument === "-h") {
40
+ options.help = true;
41
+ continue;
42
+ }
43
+ if (argument === "--target" || argument.startsWith("--target=")) {
44
+ if (options.target !== undefined) {
45
+ throw new UsageError("--target was given more than once");
46
+ }
47
+ if (argument === "--target") {
48
+ index += 1;
49
+ if (index >= argv.length) {
50
+ throw new UsageError("--target needs codex, claude or a directory");
51
+ }
52
+ options.target = argv[index];
53
+ } else {
54
+ options.target = argument.slice("--target=".length);
55
+ }
56
+ if (options.target === "") {
57
+ throw new UsageError("--target needs codex, claude or a directory");
58
+ }
59
+ continue;
60
+ }
61
+ throw new UsageError(
62
+ argument.startsWith("-") ? `unknown option: ${argument}` : `unexpected argument: ${argument}`,
63
+ );
64
+ }
65
+ return options;
66
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,232 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { InstallError } from "./errors.mjs";
7
+ import { extractZip } from "./zip.mjs";
8
+
9
+ const COMMAND_TIMEOUT_MS = 60_000;
10
+ const INSTALL_TIMEOUT_MS = 600_000;
11
+ const MANAGED_PARTS = [
12
+ "/target/debug/",
13
+ "/target/release/",
14
+ "/cellar/",
15
+ "/windowsapps/",
16
+ "/scoop/apps/",
17
+ "/chocolatey/",
18
+ ];
19
+ const MANAGED_PREFIXES = ["/nix/store/", "/snap/", "/usr/bin/", "/bin/"];
20
+
21
+ export function binaryName() {
22
+ return process.platform === "win32" ? "sqlx.exe" : "sqlx";
23
+ }
24
+
25
+ export function cliDirectory() {
26
+ const override = process.env.SQLX_INSTALL_DIR?.trim();
27
+ if (override) {
28
+ return path.resolve(override);
29
+ }
30
+ if (process.platform === "win32") {
31
+ const local = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
32
+ return path.join(local, "Programs", "SQLX");
33
+ }
34
+ return path.join(os.homedir(), ".local", "bin");
35
+ }
36
+
37
+ export function dataDirectory() {
38
+ const override = process.env.SQLX_DATA_DIR?.trim();
39
+ return override ? path.resolve(override) : path.join(os.homedir(), ".sqlx");
40
+ }
41
+
42
+ function cliFailure(stdout, stderr, status) {
43
+ for (const stream of [stdout, stderr]) {
44
+ try {
45
+ const message = JSON.parse(stream)?.error?.message;
46
+ if (typeof message === "string" && message !== "") {
47
+ return message;
48
+ }
49
+ } catch {
50
+ // Not a JSON result; fall back to the raw output.
51
+ }
52
+ }
53
+ return `${stdout ?? ""}${stderr ?? ""}`.trim() || `exit code ${status}`;
54
+ }
55
+
56
+ function run(binary, args, timeout, stage) {
57
+ const result = spawnSync(binary, args, {
58
+ encoding: "utf8",
59
+ timeout,
60
+ windowsHide: true,
61
+ env: { ...process.env, SQLX_NO_UPDATE_CHECK: "1" },
62
+ });
63
+ if (result.error) {
64
+ throw new InstallError(stage, `${path.basename(binary)} ${args.join(" ")} failed: ${result.error.message}`);
65
+ }
66
+ if (result.status !== 0) {
67
+ throw new InstallError(
68
+ stage,
69
+ `${path.basename(binary)} ${args.join(" ")} failed: ${cliFailure(result.stdout, result.stderr, result.status)}`,
70
+ );
71
+ }
72
+ return result.stdout ?? "";
73
+ }
74
+
75
+ export function readVersion(binary) {
76
+ const result = spawnSync(binary, ["--version"], {
77
+ encoding: "utf8",
78
+ timeout: COMMAND_TIMEOUT_MS,
79
+ windowsHide: true,
80
+ });
81
+ if (result.error || result.status !== 0) {
82
+ return null;
83
+ }
84
+ const match = /^sqlx (\S+) \(OtterMind\/sqlx\)$/m.exec(result.stdout ?? "");
85
+ return match ? match[1] : null;
86
+ }
87
+
88
+ export function inspect(binary) {
89
+ let stats;
90
+ try {
91
+ stats = fs.lstatSync(binary);
92
+ } catch {
93
+ return { path: binary, exists: false };
94
+ }
95
+ if (stats.isSymbolicLink()) {
96
+ throw new InstallError(
97
+ "cli",
98
+ `${binary} is a symbolic link; install SQLX with the tool that owns it, or set SQLX_INSTALL_DIR`,
99
+ );
100
+ }
101
+ if (!stats.isFile()) {
102
+ throw new InstallError("cli", `${binary} is not a regular file`);
103
+ }
104
+ const version = readVersion(binary);
105
+ if (!version) {
106
+ throw new InstallError(
107
+ "cli",
108
+ `${binary} already exists and is not an OtterMind SQLX executable; refusing to replace it`,
109
+ );
110
+ }
111
+ return { path: binary, exists: true, version };
112
+ }
113
+
114
+ function assertReplaceable(binary) {
115
+ const normalized = binary.replaceAll("\\", "/").toLowerCase();
116
+ if (
117
+ MANAGED_PARTS.some((part) => normalized.includes(part)) ||
118
+ MANAGED_PREFIXES.some((prefix) => normalized.startsWith(prefix))
119
+ ) {
120
+ throw new InstallError(
121
+ "cli",
122
+ `${binary} is owned by a package manager or a source build; update SQLX with that tool`,
123
+ );
124
+ }
125
+ if ((fs.statSync(binary).mode & 0o222) === 0) {
126
+ throw new InstallError("cli", `${binary} is read-only; update SQLX with the tool that installed it`);
127
+ }
128
+ }
129
+
130
+ export function supportsSelfUpdate(binary) {
131
+ const result = spawnSync(binary, ["update", "status"], {
132
+ encoding: "utf8",
133
+ timeout: COMMAND_TIMEOUT_MS,
134
+ windowsHide: true,
135
+ });
136
+ if (result.error || result.status !== 0) {
137
+ return false;
138
+ }
139
+ try {
140
+ return typeof JSON.parse(result.stdout)?.current_version === "string";
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ function replace(candidate, target) {
147
+ if (process.platform !== "win32" || !fs.existsSync(target)) {
148
+ fs.renameSync(candidate, target);
149
+ return;
150
+ }
151
+ // Windows cannot rename over a running executable, so keep an independent copy
152
+ // until the replacement is in place.
153
+ const backup = `${target}.sqlx-previous`;
154
+ fs.rmSync(backup, { force: true });
155
+ fs.renameSync(target, backup);
156
+ try {
157
+ fs.renameSync(candidate, target);
158
+ } catch (error) {
159
+ fs.renameSync(backup, target);
160
+ throw error;
161
+ }
162
+ fs.rmSync(backup, { force: true });
163
+ }
164
+
165
+ export function selfUpdate(binary, version) {
166
+ run(binary, ["update", "install", "--version", version], INSTALL_TIMEOUT_MS, "cli");
167
+ const installed = readVersion(binary);
168
+ if (installed !== version) {
169
+ throw new InstallError(
170
+ "cli",
171
+ `sqlx update install finished but ${binary} reports ${installed ?? "no version"}`,
172
+ );
173
+ }
174
+ }
175
+
176
+ export function installCli({ binary, bytes, entrypoint, version }) {
177
+ const directory = path.dirname(binary);
178
+ try {
179
+ fs.mkdirSync(directory, { recursive: true });
180
+ } catch (error) {
181
+ throw new InstallError("cli", `cannot create ${directory}: ${error.message}`);
182
+ }
183
+ if (fs.existsSync(binary)) {
184
+ inspect(binary);
185
+ assertReplaceable(binary);
186
+ }
187
+ let stage;
188
+ try {
189
+ stage = fs.mkdtempSync(path.join(directory, ".sqlx-npm-"));
190
+ } catch (error) {
191
+ throw new InstallError("cli", `cannot write to ${directory}: ${error.message}`);
192
+ }
193
+ try {
194
+ extractZip(bytes, stage);
195
+ const candidate = path.join(stage, entrypoint);
196
+ if (!fs.existsSync(candidate)) {
197
+ throw new InstallError("archive", `archive does not contain ${entrypoint}`);
198
+ }
199
+ fs.chmodSync(candidate, 0o755);
200
+ const candidateVersion = readVersion(candidate);
201
+ if (candidateVersion !== version) {
202
+ throw new InstallError(
203
+ "verify",
204
+ `downloaded CLI reports ${candidateVersion ?? "no version"}, expected ${version}`,
205
+ );
206
+ }
207
+ replace(candidate, binary);
208
+ } finally {
209
+ fs.rmSync(stage, { recursive: true, force: true });
210
+ }
211
+ const installed = readVersion(binary);
212
+ if (installed !== version) {
213
+ throw new InstallError("cli", `installed ${binary} reports ${installed ?? "no version"}`);
214
+ }
215
+ }
216
+
217
+ export function initializeData(binary, directory) {
218
+ if (fs.existsSync(directory)) {
219
+ return false;
220
+ }
221
+ run(binary, ["init"], INSTALL_TIMEOUT_MS, "init");
222
+ return true;
223
+ }
224
+
225
+ export function installSkill(binary, target) {
226
+ const stdout = run(binary, ["skill", "install", "--path", target], INSTALL_TIMEOUT_MS, "skill");
227
+ try {
228
+ return JSON.parse(stdout);
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
package/src/errors.mjs ADDED
@@ -0,0 +1,8 @@
1
+ export class UsageError extends Error {}
2
+
3
+ export class InstallError extends Error {
4
+ constructor(stage, message, options) {
5
+ super(message, options);
6
+ this.stage = stage;
7
+ }
8
+ }
package/src/main.mjs ADDED
@@ -0,0 +1,141 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { HELP, parseArgs } from "./args.mjs";
6
+ import * as cli from "./cli.mjs";
7
+ import { InstallError, UsageError } from "./errors.mjs";
8
+ import { compareVersions, downloadAsset, loadRelease, platformName, targetVersion } from "./release.mjs";
9
+
10
+ export function resolveSkillTarget(value, cwd) {
11
+ if (value === undefined) {
12
+ return path.resolve(cwd, "sqlx");
13
+ }
14
+ if (value === "codex") {
15
+ return path.join(os.homedir(), ".agents", "skills", "sqlx");
16
+ }
17
+ if (value === "claude") {
18
+ return path.join(os.homedir(), ".claude", "skills", "sqlx");
19
+ }
20
+ return path.resolve(cwd, value);
21
+ }
22
+
23
+ function pathHint(directory) {
24
+ if (process.platform === "win32") {
25
+ return [
26
+ `${directory} is not in PATH.`,
27
+ ` This session: $env:Path = "${directory};$env:Path"`,
28
+ " Permanent: add that directory to your user PATH",
29
+ ];
30
+ }
31
+ return [
32
+ `${directory} is not in PATH.`,
33
+ ` This shell: export PATH="${directory}:$PATH"`,
34
+ " Permanent: add that line to your shell profile",
35
+ ];
36
+ }
37
+
38
+ function report(progress, log) {
39
+ log.log(`SQLX ${progress.cli.version}`);
40
+ log.log(` CLI ${progress.cli.path} (${progress.cli.action})`);
41
+ log.log(` Skill ${progress.skill.path}${progress.skill.version ? ` (${progress.skill.version})` : ""}`);
42
+ log.log(` Data ${progress.data.path} (${progress.data.created ? "created" : "already initialized"})`);
43
+ const directory = path.dirname(progress.cli.path);
44
+ const entries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
45
+ if (!entries.some((entry) => path.resolve(entry) === directory)) {
46
+ log.log("");
47
+ for (const line of pathHint(directory)) {
48
+ log.log(line);
49
+ }
50
+ }
51
+ log.log("");
52
+ log.log("Next: sqlx --version && sqlx skill status");
53
+ }
54
+
55
+ async function install(options, cwd, log) {
56
+ const version = targetVersion();
57
+ const platform = platformName();
58
+ const binary = path.join(cli.cliDirectory(), cli.binaryName());
59
+ const skillTarget = resolveSkillTarget(options.target, cwd);
60
+ const progress = { cli: null, skill: null, data: null };
61
+
62
+ // Refuse an unusable Skill destination before changing anything else.
63
+ let stats = null;
64
+ try {
65
+ stats = fs.lstatSync(skillTarget);
66
+ } catch {
67
+ stats = null;
68
+ }
69
+ if (stats?.isSymbolicLink()) {
70
+ throw new InstallError("skill", `${skillTarget} is a symbolic link; choose another --target`);
71
+ }
72
+
73
+ const existing = cli.inspect(binary);
74
+ let action;
75
+ if (!existing.exists) {
76
+ action = "install";
77
+ } else if (existing.version === version) {
78
+ action = "reuse";
79
+ } else if (compareVersions(existing.version, version) > 0) {
80
+ action = "keep";
81
+ } else {
82
+ action = cli.supportsSelfUpdate(binary) ? "update" : "replace";
83
+ }
84
+
85
+ if (action === "install" || action === "replace") {
86
+ const release = await loadRelease(version, platform);
87
+ const bytes = await downloadAsset(release.asset);
88
+ cli.installCli({ binary, bytes, entrypoint: release.asset.entrypoint, version });
89
+ progress.cli = { path: binary, version, action: action === "install" ? "installed" : "replaced" };
90
+ } else if (action === "update") {
91
+ cli.selfUpdate(binary, version);
92
+ progress.cli = { path: binary, version, action: "updated" };
93
+ } else if (action === "reuse") {
94
+ progress.cli = { path: binary, version, action: "reused" };
95
+ } else {
96
+ progress.cli = { path: binary, version: existing.version, action: "kept newer version" };
97
+ }
98
+
99
+ const dataDirectory = cli.dataDirectory();
100
+ progress.data = { path: dataDirectory, created: cli.initializeData(binary, dataDirectory) };
101
+
102
+ try {
103
+ const result = cli.installSkill(binary, skillTarget);
104
+ progress.skill = { path: skillTarget, version: result?.version ?? null };
105
+ } catch (error) {
106
+ throw new InstallError(
107
+ "skill",
108
+ `${error.message}\nThe CLI at ${binary} (${progress.cli.version}) is installed and usable; fix the Skill destination and run the command again to finish the Skill step.`,
109
+ { cause: error },
110
+ );
111
+ }
112
+
113
+ report(progress, log);
114
+ return 0;
115
+ }
116
+
117
+ export async function run(argv, log = console) {
118
+ const cwd = process.cwd();
119
+ let options;
120
+ try {
121
+ options = parseArgs(argv);
122
+ } catch (error) {
123
+ if (!(error instanceof UsageError)) {
124
+ throw error;
125
+ }
126
+ log.error(`sqlx-install: ${error.message}`);
127
+ log.error("Run with --help for usage.");
128
+ return 2;
129
+ }
130
+ if (options.help) {
131
+ log.log(HELP);
132
+ return 0;
133
+ }
134
+ try {
135
+ return await install(options, cwd, log);
136
+ } catch (error) {
137
+ const stage = error instanceof InstallError ? error.stage : "install";
138
+ log.error(`SQLX installation failed (${stage}): ${error.message}`);
139
+ return 1;
140
+ }
141
+ }
@@ -0,0 +1,177 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+
4
+ import { InstallError } from "./errors.mjs";
5
+
6
+ const DEFAULT_RELEASE_BASE = "https://github.com/OtterMind/sqlx/releases";
7
+ const METADATA_TIMEOUT_MS = 30_000;
8
+ const ARCHIVE_TIMEOUT_MS = 600_000;
9
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
10
+
11
+ let metadata;
12
+
13
+ function packageMetadata() {
14
+ metadata ??= JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
15
+ return metadata;
16
+ }
17
+
18
+ export function targetVersion() {
19
+ return process.env.SQLX_VERSION?.trim() || packageMetadata().version;
20
+ }
21
+
22
+ export function platformName() {
23
+ const os = { darwin: "macos", linux: "linux", win32: "windows" }[process.platform];
24
+ if (!os) {
25
+ throw new InstallError("platform", `unsupported operating system: ${process.platform}`);
26
+ }
27
+ const arch = { arm64: "arm64", x64: "x64" }[process.arch];
28
+ if (!arch) {
29
+ throw new InstallError("platform", `unsupported CPU architecture: ${process.arch}`);
30
+ }
31
+ if (os === "windows" && arch !== "x64") {
32
+ throw new InstallError("platform", "Windows ARM64 is not a release target");
33
+ }
34
+ return `${os}-${arch}`;
35
+ }
36
+
37
+ export function releaseBase() {
38
+ return (process.env.SQLX_RELEASE_BASE?.trim() || DEFAULT_RELEASE_BASE).replace(/\/+$/, "");
39
+ }
40
+
41
+ export function compareVersions(left, right) {
42
+ const parts = (value) => {
43
+ const numbers = value.split(".").map((part) => (/^\d+$/.test(part) ? Number(part) : Number.NaN));
44
+ if (numbers.some(Number.isNaN)) {
45
+ throw new InstallError("version", `cannot compare versions ${left} and ${right}`);
46
+ }
47
+ return numbers;
48
+ };
49
+ const a = parts(left);
50
+ const b = parts(right);
51
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
52
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
53
+ if (difference !== 0) {
54
+ return Math.sign(difference);
55
+ }
56
+ }
57
+ return 0;
58
+ }
59
+
60
+ export function sha256(bytes) {
61
+ return createHash("sha256").update(bytes).digest("hex");
62
+ }
63
+
64
+ function allowedUrl(value) {
65
+ let url;
66
+ try {
67
+ url = new URL(value);
68
+ } catch {
69
+ return false;
70
+ }
71
+ if (url.username || url.password || url.search || url.hash) {
72
+ return false;
73
+ }
74
+ if (url.protocol === "https:") {
75
+ return true;
76
+ }
77
+ return url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
78
+ }
79
+
80
+ function releaseUrl(base, version, file) {
81
+ return `${base}/download/v${version}/${file}`;
82
+ }
83
+
84
+ async function fetchBytes(url, timeout, what) {
85
+ let response;
86
+ try {
87
+ response = await fetch(url, {
88
+ redirect: "follow",
89
+ signal: AbortSignal.timeout(timeout),
90
+ headers: { "user-agent": `${packageMetadata().name}/${packageMetadata().version}` },
91
+ });
92
+ } catch (error) {
93
+ throw new InstallError("download", `could not download ${what} from ${url}: ${error.message}`);
94
+ }
95
+ if (!response.ok) {
96
+ throw new InstallError("download", `could not download ${what} from ${url}: HTTP ${response.status}`);
97
+ }
98
+ return Buffer.from(await response.arrayBuffer());
99
+ }
100
+
101
+ function parseSums(text) {
102
+ const sums = new Map();
103
+ for (const line of text.split("\n")) {
104
+ const match = /^([0-9a-fA-F]{64})\s+\*?(.+?)\s*$/.exec(line);
105
+ if (match) {
106
+ sums.set(match[2], match[1].toLowerCase());
107
+ }
108
+ }
109
+ return sums;
110
+ }
111
+
112
+ function validEntrypoint(value) {
113
+ return (
114
+ typeof value === "string" &&
115
+ value.length > 0 &&
116
+ value !== "." &&
117
+ value !== ".." &&
118
+ !value.includes("/") &&
119
+ !value.includes("\\")
120
+ );
121
+ }
122
+
123
+ export async function loadRelease(version, platform) {
124
+ const base = releaseBase();
125
+ const sums = parseSums(
126
+ (await fetchBytes(releaseUrl(base, version, "SHA256SUMS"), METADATA_TIMEOUT_MS, "SHA256SUMS")).toString("utf8"),
127
+ );
128
+ const manifestBytes = await fetchBytes(
129
+ releaseUrl(base, version, "manifest.json"),
130
+ METADATA_TIMEOUT_MS,
131
+ "manifest.json",
132
+ );
133
+ if (sums.get("manifest.json") !== sha256(manifestBytes)) {
134
+ throw new InstallError("verify", "SHA256SUMS does not match the downloaded manifest.json");
135
+ }
136
+ let manifest;
137
+ try {
138
+ manifest = JSON.parse(manifestBytes.toString("utf8"));
139
+ } catch (error) {
140
+ throw new InstallError("verify", `release manifest is not valid JSON: ${error.message}`);
141
+ }
142
+ if (manifest?.schema_version !== 1) {
143
+ throw new InstallError("verify", "unsupported release manifest schema");
144
+ }
145
+ const asset = manifest.components?.[`cli:${platform}`];
146
+ if (!asset) {
147
+ throw new InstallError("verify", `release ${version} has no CLI build for ${platform}`);
148
+ }
149
+ if (asset.version !== version) {
150
+ throw new InstallError("verify", `release manifest CLI version ${asset.version} does not match ${version}`);
151
+ }
152
+ if (asset.archive !== "zip") {
153
+ throw new InstallError("verify", `unsupported CLI archive format: ${asset.archive}`);
154
+ }
155
+ if (!validEntrypoint(asset.entrypoint)) {
156
+ throw new InstallError("verify", `unsafe CLI entrypoint: ${asset.entrypoint}`);
157
+ }
158
+ if (!/^[0-9a-fA-F]{64}$/.test(asset.sha256 ?? "")) {
159
+ throw new InstallError("verify", "release manifest has an invalid CLI SHA-256");
160
+ }
161
+ if (!allowedUrl(asset.url)) {
162
+ throw new InstallError("verify", `unsafe CLI download URL: ${asset.url}`);
163
+ }
164
+ const name = new URL(asset.url).pathname.split("/").pop();
165
+ if (!name || sums.get(name) !== asset.sha256.toLowerCase()) {
166
+ throw new InstallError("verify", `SHA256SUMS and manifest disagree about ${name || "the CLI archive"}`);
167
+ }
168
+ return { version, platform, asset: { ...asset, name } };
169
+ }
170
+
171
+ export async function downloadAsset(asset) {
172
+ const bytes = await fetchBytes(asset.url, ARCHIVE_TIMEOUT_MS, asset.name);
173
+ if (sha256(bytes) !== asset.sha256.toLowerCase()) {
174
+ throw new InstallError("verify", `checksum verification failed for ${asset.name}`);
175
+ }
176
+ return bytes;
177
+ }
package/src/zip.mjs ADDED
@@ -0,0 +1,122 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import zlib from "node:zlib";
4
+
5
+ import { InstallError } from "./errors.mjs";
6
+
7
+ const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
8
+ const CENTRAL_FILE_HEADER = 0x02014b50;
9
+ const LOCAL_FILE_HEADER = 0x04034b50;
10
+ const ZIP64_MARKER = 0xffffffff;
11
+ const MAXIMUM_COMMENT = 0xffff;
12
+ const UNIX_FILE_TYPE = 0xf000;
13
+ const UNIX_SYMBOLIC_LINK = 0xa000;
14
+ const UNIX_DIRECTORY = 0x4000;
15
+
16
+ function centralDirectoryEnd(buffer) {
17
+ const earliest = Math.max(0, buffer.length - MAXIMUM_COMMENT - 22);
18
+ for (let offset = buffer.length - 22; offset >= earliest; offset -= 1) {
19
+ if (buffer.readUInt32LE(offset) === END_OF_CENTRAL_DIRECTORY) {
20
+ return offset;
21
+ }
22
+ }
23
+ throw new InstallError("archive", "downloaded archive is not a ZIP file");
24
+ }
25
+
26
+ function safeName(raw) {
27
+ if (raw.includes("\\") || raw.includes(":")) {
28
+ return null;
29
+ }
30
+ const trimmed = raw.replace(/\/+$/, "");
31
+ if (trimmed === "") {
32
+ return null;
33
+ }
34
+ const parts = trimmed.split("/");
35
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
36
+ return null;
37
+ }
38
+ return trimmed;
39
+ }
40
+
41
+ export function extractZip(buffer, destination) {
42
+ const end = centralDirectoryEnd(buffer);
43
+ const entries = buffer.readUInt16LE(end + 10);
44
+ const centralSize = buffer.readUInt32LE(end + 12);
45
+ const centralOffset = buffer.readUInt32LE(end + 16);
46
+ if (centralOffset === ZIP64_MARKER || centralSize === ZIP64_MARKER || entries === 0xffff) {
47
+ throw new InstallError("archive", "ZIP64 archives are not supported");
48
+ }
49
+ if (centralOffset + centralSize > buffer.length) {
50
+ throw new InstallError("archive", "ZIP central directory is out of bounds");
51
+ }
52
+ const written = [];
53
+ let offset = centralOffset;
54
+ for (let index = 0; index < entries; index += 1) {
55
+ if (offset + 46 > buffer.length || buffer.readUInt32LE(offset) !== CENTRAL_FILE_HEADER) {
56
+ throw new InstallError("archive", "ZIP central directory is damaged");
57
+ }
58
+ const flags = buffer.readUInt16LE(offset + 8);
59
+ const method = buffer.readUInt16LE(offset + 10);
60
+ const compressedSize = buffer.readUInt32LE(offset + 20);
61
+ const uncompressedSize = buffer.readUInt32LE(offset + 24);
62
+ const nameLength = buffer.readUInt16LE(offset + 28);
63
+ const extraLength = buffer.readUInt16LE(offset + 30);
64
+ const commentLength = buffer.readUInt16LE(offset + 32);
65
+ const externalAttributes = buffer.readUInt32LE(offset + 38);
66
+ const localOffset = buffer.readUInt32LE(offset + 42);
67
+ if (offset + 46 + nameLength > buffer.length) {
68
+ throw new InstallError("archive", "ZIP central directory is damaged");
69
+ }
70
+ const rawName = buffer.toString("utf8", offset + 46, offset + 46 + nameLength);
71
+ offset += 46 + nameLength + extraLength + commentLength;
72
+
73
+ if (flags & 0x1) {
74
+ throw new InstallError("archive", "encrypted ZIP entries are not supported");
75
+ }
76
+ if (
77
+ compressedSize === ZIP64_MARKER ||
78
+ uncompressedSize === ZIP64_MARKER ||
79
+ localOffset === ZIP64_MARKER
80
+ ) {
81
+ throw new InstallError("archive", "ZIP64 entries are not supported");
82
+ }
83
+ if (((externalAttributes >>> 16) & UNIX_FILE_TYPE) === UNIX_SYMBOLIC_LINK) {
84
+ throw new InstallError("archive", `ZIP symbolic links are not supported: ${rawName}`);
85
+ }
86
+ const name = safeName(rawName);
87
+ if (name === null) {
88
+ throw new InstallError("archive", `unsafe archive path: ${rawName}`);
89
+ }
90
+ if (method !== 0 && method !== 8) {
91
+ throw new InstallError("archive", `unsupported ZIP compression method ${method} for ${name}`);
92
+ }
93
+ if (localOffset + 30 > buffer.length || buffer.readUInt32LE(localOffset) !== LOCAL_FILE_HEADER) {
94
+ throw new InstallError("archive", `ZIP local header is damaged for ${name}`);
95
+ }
96
+ const start =
97
+ localOffset + 30 + buffer.readUInt16LE(localOffset + 26) + buffer.readUInt16LE(localOffset + 28);
98
+ if (start + compressedSize > buffer.length) {
99
+ throw new InstallError("archive", `ZIP entry data is out of bounds for ${name}`);
100
+ }
101
+ const target = path.join(destination, name);
102
+ if (((externalAttributes >>> 16) & UNIX_FILE_TYPE) === UNIX_DIRECTORY || rawName.endsWith("/")) {
103
+ fs.mkdirSync(target, { recursive: true });
104
+ continue;
105
+ }
106
+ const raw = buffer.subarray(start, start + compressedSize);
107
+ let content;
108
+ try {
109
+ content = method === 0 ? raw : zlib.inflateRawSync(raw);
110
+ } catch (error) {
111
+ throw new InstallError("archive", `could not decompress ${name}: ${error.message}`);
112
+ }
113
+ if (content.length !== uncompressedSize) {
114
+ throw new InstallError("archive", `ZIP entry ${name} has an unexpected size`);
115
+ }
116
+ fs.mkdirSync(path.dirname(target), { recursive: true });
117
+ const mode = (externalAttributes >>> 16) & 0o777;
118
+ fs.writeFileSync(target, content, { mode: mode || 0o644, flag: "wx" });
119
+ written.push(name);
120
+ }
121
+ return written;
122
+ }