@nocobase/app-cli-distribution 0.1.0-beta.0 → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,9 +43,37 @@ app.route('/api/agents/dist', distribution);
43
43
 
44
44
  No artifact is public by default. `admissions` are ordered asynchronous handlers: return `false` when the handler recognizes no credential, return `true` after authorizing it, and throw on an invalid recognized credential. An error never falls through to another handler or a signed-in session. After every handler declines, `fallback` may authorize through the host. With neither an accepting handler nor a fallback, requests receive 401 `UNAUTHORIZED` in the existing `agents` error domain. Never use download credentials as business login credentials.
45
45
 
46
- The package implements no public-download strategy or signed credential protocol. A host can write its own authorization middleware, which makes its security policy the host's responsibility. Choosing Main's policy, any public access, and any new persistent token store require a separate product decision. This package does not select one or create a table.
46
+ The package implements no anonymous artifact admission or signed credential protocol. A host can write its own authorization middleware, which makes its security policy the host's responsibility. Choosing Main's policy, any public access, and any new persistent token store require a separate product decision. This package does not select one or create a table.
47
47
 
48
- `createInstallRoutes` serves a public script containing no credential. The CLI-only script accepts `--token`, `--server`, `--prefix`, `--bin-dir`, `--dry-run`, `-h` and `--help`; it sends the token in `x-nocobase-download-token`. A session-only host can resolve/download/update through its authenticated clients, but must supply an installation credential admission if it wants this token-based one-line installation. The `InstallScriptExtensions.credentials` hook may implement another host-approved installation transport. No Runner flags, registration or services are in the default script.
48
+ `createInstallRoutes` and `installScript` keep the protected CLI installation protocol unchanged. They accept `--token`, `--server`, `--prefix`, `--bin-dir`, `--dry-run`, `-h` and `--help`, and send `x-nocobase-download-token`. Agents and its Runner extensions keep using these original entry points. The script implementation and extension contract are not shared with public-mode generation.
49
+
50
+ ## Independent public installer
51
+
52
+ A host that deliberately serves its own CLI artifacts publicly imports a separate entry:
53
+
54
+ ```ts
55
+ import {
56
+ publicInstallScript,
57
+ createPublicInstallRoutes,
58
+ } from '@nocobase/app-cli-distribution/public-install';
59
+
60
+ // Either render the script in the host's own route:
61
+ const script = publicInstallScript({
62
+ cli: 'acme',
63
+ server: 'https://example.com/platform/main',
64
+ });
65
+ // Or mount this router alongside that host's public artifact router:
66
+ const installation = createPublicInstallRoutes({
67
+ cli: () => 'acme',
68
+ publicBasePath: '/platform/main',
69
+ });
70
+ ```
71
+
72
+ Mount the router at the host's existing `/api/agents/dist` prefix, choosing one installation router. This script has no token parsing, credential header, Runner registration, service installation or extension hooks. It independently handles server/prefix/bin-dir/dry-run, platform/tool checks, anonymous resolution/download, SHA-256 verification, installation metadata, version activation, command links and the next login instruction. It requires Node.js 24+, curl, tar and a SHA-256 tool on macOS/Linux (Windows through WSL); npm is required only for an npm resolution. It reuses the stateless npm launcher and requested-server helper, not the protected script's text. Relative installation paths become absolute, and metadata remains valid for paths containing quotes. Dry-run resolves metadata but performs no download or installation writes. Exit 0 indicates success/help/dry-run, 2 invalid usage/address and 1 runtime/prerequisite/resolution/checksum failures; a failing external install tool may retain its own nonzero code.
73
+
74
+ The host must separately configure anonymous artifact admission, restricted to its CLI product, for example `products: ['acme']` with `admissions: [async () => true]`. The new script does not change artifact authorization. A protected response fails even if an older CLI is present. Keep deployment configuration, database files and credentials out of the public archive.
75
+
76
+ Existing `<cli> update` already consumes the same resolution/file endpoints after selecting its signed-in profile, and needs no download token when those endpoints are public. No `publicUpdates` brand option or `update --server` extension is provided. A never-signed-in installation can rerun the public installer; normal business use still begins with native login. The old update command, protected distribution targets and Runner ownership remain unchanged.
49
77
 
50
78
  ## Optional stateful download tokens
51
79
 
@@ -77,4 +105,4 @@ The new package's host tests exercise protected defaults, an explicit session po
77
105
 
78
106
  The extraction adds no database query, write, lock or transaction to an Agents request and no polling or Runner claim work. Token mint remains one insert/transaction; metadata admission one read/transaction plus a conditional update when binding; a token file request remains two reads/transactions and one conditional counting update. The product restriction adds an in-memory membership check. No independent-host database or jobs are required.
79
107
 
80
- Main integration and UI, its authorization choice, signed tokens/public downloads, token cleanup, mode switching between independent distribution and Agents, and the client's full-buffer update download remain separate work. One router owns `/api/agents/dist` in a host; do not register both independent distribution and Agents at that prefix.
108
+ Main integration and UI, host authorization choices, token cleanup, mode switching between independent distribution and Agents, and the client's full-buffer update download remain separate work. One router owns `/api/agents/dist` in a host; do not register both independent distribution and Agents at that prefix.
@@ -0,0 +1,17 @@
1
+ import { type DescribeRouteOptions } from '@nocobase/app-server/router';
2
+ import { Hono } from 'hono';
3
+ export interface PublicInstallScriptOptions {
4
+ readonly cli: string;
5
+ readonly server?: string;
6
+ }
7
+ /** A separate CLI-only installer. It neither changes nor extends the protected installation protocol. */
8
+ export declare function publicInstallScript(options: PublicInstallScriptOptions): string;
9
+ export interface PublicInstallRoutesOptions {
10
+ readonly cli: () => string;
11
+ readonly publicOrigin?: () => string | undefined;
12
+ readonly publicBasePath?: string;
13
+ readonly openapi?: DescribeRouteOptions;
14
+ }
15
+ /** Mount explicitly alongside a host's independently configured public artifact admission. */
16
+ export declare function createPublicInstallRoutes(options: PublicInstallRoutesOptions): Hono;
17
+ //# sourceMappingURL=public-install.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-install.d.ts","sourceRoot":"","sources":["../src/public-install.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAK5B,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,yGAAyG;AACzG,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,MAAM,CAWR;AAED,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACjD,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;CACzC;AAED,8FAA8F;AAC9F,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,IAAI,CAgCN"}
@@ -0,0 +1,164 @@
1
+ import { DIST_PRODUCT_PATTERN } from '@nocobase/agent-protocol';
2
+ import { npmLauncherScript } from '@nocobase/app-cli-client/install';
3
+ import { apiErrorResponse, cliRoute, describeRoute, } from '@nocobase/app-server/router';
4
+ import { Hono } from 'hono';
5
+ import { requestedServer } from './install.js';
6
+ const SAFE_SERVER = /^https?:\/\/[A-Za-z0-9.:[\]_-]+(?:\/[A-Za-z0-9._~/-]*)?$/u;
7
+ /** A separate CLI-only installer. It neither changes nor extends the protected installation protocol. */
8
+ export function publicInstallScript(options) {
9
+ if (!DIST_PRODUCT_PATTERN.test(options.cli) ||
10
+ options.cli === 'nocobase-runner')
11
+ throw new Error(`Not a CLI command name: ${options.cli}`);
12
+ const server = options.server ?? '';
13
+ return PUBLIC_INSTALL_SCRIPT.replaceAll('@CLI@', options.cli).replace('@DEFAULT_SERVER@', SAFE_SERVER.test(server) ? server.replace(/\/+$/u, '') : '');
14
+ }
15
+ /** Mount explicitly alongside a host's independently configured public artifact admission. */
16
+ export function createPublicInstallRoutes(options) {
17
+ return new Hono().get('/installScript', describeRoute(options.openapi ?? {
18
+ tags: ['CliDistribution'],
19
+ summary: 'Get the public CLI install script',
20
+ operationId: 'cliDistributionPublicInstallScript',
21
+ ...cliRoute(false),
22
+ security: [],
23
+ responses: {
24
+ 200: {
25
+ description: 'A public CLI-only POSIX shell script.',
26
+ content: { 'text/x-shellscript': { schema: { type: 'string' } } },
27
+ },
28
+ 500: apiErrorResponse(500),
29
+ },
30
+ }), (context) => context.body(publicInstallScript({
31
+ cli: options.cli(),
32
+ server: requestedServer(context, options),
33
+ }), 200, {
34
+ 'content-type': 'text/x-shellscript; charset=utf-8',
35
+ 'cache-control': 'no-cache',
36
+ }));
37
+ }
38
+ // Kept independent of the protected script and its extension points, intentionally allowing some duplication.
39
+ const PUBLIC_INSTALL_SCRIPT = `#!/bin/sh
40
+ # Installs the publicly downloadable @CLI@ CLI. Business access requires a separate login.
41
+ set -eu
42
+ usage() {
43
+ cat <<'USAGE'
44
+ Usage: install.sh [--server <url>] [--prefix <dir>] [--bin-dir <dir>] [--dry-run]
45
+
46
+ Requires Node.js 24+, curl, tar and a SHA-256 tool. Supports macOS/Linux and Windows through WSL.
47
+ USAGE
48
+ }
49
+ cli=@CLI@
50
+ server="@DEFAULT_SERVER@"
51
+ prefix="\${NOCOBASE_CLI_INSTALL_DIR:-}"
52
+ bin_dir="\${NOCOBASE_CLI_BIN_DIR:-}"
53
+ dry_run=0
54
+ while [ $# -gt 0 ]; do
55
+ case "$1" in
56
+ --server|--prefix|--bin-dir)
57
+ [ $# -ge 2 ] && [ -n "$2" ] || { usage >&2; exit 2; }
58
+ case "$1" in
59
+ --server) server="$2" ;;
60
+ --prefix) prefix="$2" ;;
61
+ --bin-dir) bin_dir="$2" ;;
62
+ esac
63
+ shift 2 ;;
64
+ --dry-run) dry_run=1; shift ;;
65
+ -h|--help) usage; exit 0 ;;
66
+ *) echo 'Unknown installation option.' >&2; usage >&2; exit 2 ;;
67
+ esac
68
+ done
69
+ [ -n "$server" ] || { usage >&2; exit 2; }
70
+ say() { printf '%s\\n' "$*"; }
71
+ fail() { printf '%s install: %s\\n' "$cli" "$*" >&2; exit 1; }
72
+ run() { if [ "$dry_run" = 1 ]; then say "+ $*"; else "$@"; fi; }
73
+ case "$(uname -s)" in
74
+ Darwin) os=darwin ;;
75
+ Linux) os=linux ;;
76
+ *) fail 'Use macOS, Linux or WSL.' ;;
77
+ esac
78
+ case "$(uname -m)" in
79
+ arm64|aarch64) arch=arm64 ;;
80
+ x86_64|amd64) arch=x64 ;;
81
+ *) fail 'Unsupported processor.' ;;
82
+ esac
83
+ target="\${NOCOBASE_CLI_TARGET:-$os-$arch}"
84
+ node_bin="$(command -v node 2>/dev/null || true)"
85
+ [ -n "$node_bin" ] || fail 'Node.js 24 or newer is required.'
86
+ node_version="$("$node_bin" -p 'process.versions.node' 2>/dev/null || true)"
87
+ case "$node_version" in ''|*[!0-9.]*) fail 'Node.js 24 or newer is required.' ;; esac
88
+ [ "\${node_version%%.*}" -ge 24 ] || fail 'Node.js 24 or newer is required.'
89
+ # Validate without printing an invalid address, which could accidentally contain a credential.
90
+ server="$("$node_bin" -e 'try { const u = new URL(process.argv[1]); if (!["http:", "https:"].includes(u.protocol) || u.username || u.password || u.search || u.hash) throw Error(); console.log(u.href.replace(/\\/+$/, "")); } catch { process.exit(2); }' "$server")" || { echo 'Use an HTTP(S) application URL without credentials, query or fragment.' >&2; exit 2; }
91
+ prefix="\${prefix:-$HOME/.local/share/$cli}"
92
+ bin_dir="\${bin_dir:-$HOME/.local/bin}"
93
+ prefix="$("$node_bin" -e 'console.log(require("node:path").resolve(process.argv[1]))' "$prefix")"
94
+ bin_dir="$("$node_bin" -e 'console.log(require("node:path").resolve(process.argv[1]))' "$bin_dir")"
95
+ command -v curl >/dev/null 2>&1 || fail 'curl is required.'
96
+ command -v tar >/dev/null 2>&1 || fail 'tar is required.'
97
+ if command -v sha256sum >/dev/null 2>&1; then
98
+ sha256() { sha256sum "$1" | cut -d ' ' -f 1; }
99
+ elif command -v shasum >/dev/null 2>&1; then
100
+ sha256() { shasum -a 256 "$1" | cut -d ' ' -f 1; }
101
+ else
102
+ fail 'sha256sum or shasum is required.'
103
+ fi
104
+ tmp="$(mktemp -d)"
105
+ trap 'rm -rf "$tmp"' EXIT INT TERM
106
+ field() { sed -n "s/^$1=//p" "$tmp/resolve" | head -n 1; }
107
+ status="$(curl -sS -o "$tmp/resolve" -w '%{http_code}' "$server/api/agents/dist/products/$cli/targets/$target?format=env&accept=npm")" || fail 'Could not reach the application.'
108
+ [ "$status" = 200 ] || fail "The application answered $status while resolving the CLI."
109
+ version="$(field version)"
110
+ case "$version" in ''|.|..|*[!0-9A-Za-z.+-]*) fail 'The application named an unexpected version.' ;; esac
111
+ kind="$(field kind)"
112
+ dest="$prefix/versions/$version"
113
+ if [ "$kind" = npm ]; then
114
+ package="$(field package)"
115
+ case "$package" in ''|*[!0-9a-z@/._~-]*) fail 'The application named an unexpected npm package.' ;; esac
116
+ npm_bin="\${NOCOBASE_NPM:-$(command -v npm 2>/dev/null || true)}"
117
+ [ -n "$npm_bin" ] && [ -x "$npm_bin" ] || fail 'npm is required for this package.'
118
+ else
119
+ url="$(field url)"
120
+ checksum="$(field sha256)"
121
+ case "$url" in /*) ;; *) fail 'The application named an unexpected download.' ;; esac
122
+ case "$checksum" in *[!0-9a-f]*|'') fail 'The application named an unexpected checksum.' ;; esac
123
+ fi
124
+ if [ ! -x "$dest/bin/$cli" ]; then
125
+ if [ "$kind" = npm ]; then
126
+ run rm -rf "$dest.partial"
127
+ run mkdir -p "$dest.partial"
128
+ run "$npm_bin" install --prefix "$dest.partial" --no-save --no-audit --no-fund --omit=optional "$package@$version"
129
+ if [ "$dry_run" = 0 ]; then
130
+ [ -e "$dest.partial/node_modules/.bin/$cli" ] || fail 'The package provides no CLI command.'
131
+ mkdir -p "$dest.partial/bin"
132
+ cat <<'LAUNCHER' >"$dest.partial/bin/$cli"
133
+ ${npmLauncherScript()}LAUNCHER
134
+ chmod 755 "$dest.partial/bin/$cli"
135
+ fi
136
+ run mv "$dest.partial" "$dest"
137
+ else
138
+ run curl -fSL --progress-bar -o "$tmp/cli.tar.gz" "$server$url" || fail 'CLI download failed.'
139
+ if [ "$dry_run" = 0 ]; then
140
+ actual="$(sha256 "$tmp/cli.tar.gz")"
141
+ [ "$actual" = "$checksum" ] || fail 'The download does not match its SHA-256.'
142
+ say 'Checksum verified.'
143
+ fi
144
+ run rm -rf "$dest.partial"
145
+ run mkdir -p "$dest.partial"
146
+ run tar -xzf "$tmp/cli.tar.gz" -C "$dest.partial" --strip-components=1 || fail 'Could not unpack the CLI archive.'
147
+ if [ "$dry_run" = 0 ]; then
148
+ [ -x "$dest.partial/bin/$cli" ] || fail 'The archive provides no CLI command.'
149
+ fi
150
+ run mv "$dest.partial" "$dest"
151
+ fi
152
+ fi
153
+ run ln -sfn "versions/$version" "$prefix/current"
154
+ run ln -sfn "$node_bin" "$prefix/node"
155
+ run mkdir -p "$bin_dir"
156
+ run ln -sfn "$prefix/current/bin/$cli" "$bin_dir/$cli"
157
+ if [ "$dry_run" = 0 ]; then
158
+ "$node_bin" -e 'console.log(JSON.stringify({prefix: process.argv[1], binLink: process.argv[2], mode: "cli"}, null, 2))' "$prefix" "$bin_dir/$cli" >"$prefix/install.json"
159
+ fi
160
+ if [ "$dry_run" = 1 ]; then say 'Dry run: no installation was changed.'; else say "$cli is installed: $bin_dir/$cli"; fi
161
+ case ":$PATH:" in *":$bin_dir:"*) ;; *) say "Add $bin_dir to your PATH." ;; esac
162
+ say "Next: $cli login --server $server"
163
+ `;
164
+ //# sourceMappingURL=public-install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-install.js","sourceRoot":"","sources":["../src/public-install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,EACL,gBAAgB,EAChB,QAAQ,EACR,aAAa,GAEd,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,MAAM,WAAW,GAAG,2DAA2D,CAAC;AAOhF,yGAAyG;AACzG,MAAM,UAAU,mBAAmB,CACjC,OAAmC;IAEnC,IACE,CAAC,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;QACvC,OAAO,CAAC,GAAG,KAAK,iBAAiB;QAEjC,MAAM,IAAI,KAAK,CAAC,2BAA2B,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC;IACpC,OAAO,qBAAqB,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CACnE,kBAAkB,EAClB,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAC5D,CAAC;AACJ,CAAC;AASD,8FAA8F;AAC9F,MAAM,UAAU,yBAAyB,CACvC,OAAmC;IAEnC,OAAO,IAAI,IAAI,EAAE,CAAC,GAAG,CACnB,gBAAgB,EAChB,aAAa,CACX,OAAO,CAAC,OAAO,IAAI;QACjB,IAAI,EAAE,CAAC,iBAAiB,CAAC;QACzB,OAAO,EAAE,mCAAmC;QAC5C,WAAW,EAAE,oCAAoC;QACjD,GAAG,QAAQ,CAAC,KAAK,CAAC;QAClB,QAAQ,EAAE,EAAE;QACZ,SAAS,EAAE;YACT,GAAG,EAAE;gBACH,WAAW,EAAE,uCAAuC;gBACpD,OAAO,EAAE,EAAE,oBAAoB,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE;aAClE;YACD,GAAG,EAAE,gBAAgB,CAAC,GAAG,CAAC;SAC3B;KACF,CACF,EACD,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,IAAI,CACV,mBAAmB,CAAC;QAClB,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QAClB,MAAM,EAAE,eAAe,CAAC,OAAO,EAAE,OAAO,CAAC;KAC1C,CAAC,EACF,GAAG,EACH;QACE,cAAc,EAAE,mCAAmC;QACnD,eAAe,EAAE,UAAU;KAC5B,CACF,CACJ,CAAC;AACJ,CAAC;AAED,8GAA8G;AAC9G,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8F5B,iBAAiB,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BpB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/app-cli-distribution",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.1",
4
4
  "description": "Composable server-side CLI artifact distribution and installation.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -15,6 +15,10 @@
15
15
  ".": {
16
16
  "types": "./dist/index.d.ts",
17
17
  "import": "./dist/index.js"
18
+ },
19
+ "./public-install": {
20
+ "types": "./dist/public-install.d.ts",
21
+ "import": "./dist/public-install.js"
18
22
  }
19
23
  },
20
24
  "files": [
@@ -27,10 +31,10 @@
27
31
  "hono": "^4.13.2",
28
32
  "zod": "^4.6.6",
29
33
  "@nocobase/agent-protocol": "^0.1.0-beta.19",
30
- "@nocobase/app-cli-client": "^0.1.0-beta.21"
34
+ "@nocobase/app-cli-client": "^0.1.0-beta.22"
31
35
  },
32
36
  "peerDependencies": {
33
- "@nocobase/app-server": "^5.0.0-beta.5"
37
+ "@nocobase/app-server": "^5.0.0-beta.6"
34
38
  },
35
39
  "devDependencies": {
36
40
  "@types/node": "^24.0.0",