@cityholdings/minicube-dev 0.0.0-stage → 0.1.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
@@ -1,3 +1,157 @@
1
- # Temporary Holding Version
1
+ # minicube-dev
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ One command-line workflow for mPaaS **mini programs**, **H5 offline packages** and **Cube cards** —
4
+ from code to build, device preview and release, with no IDE or console UI.
5
+
6
+ | Step | Mini program / H5 offline package | Card |
7
+ | --- | --- | --- |
8
+ | Build | `minicube-dev build` | `minicube-dev build` (`cube-pack build` if installed, else `act build`) |
9
+ | Debug | `minicube-dev debug --device android` — uploads a debug package visible only to a whitelist and opens it on the device | `minicube-dev debug --device ios-sim` — pushed over the local ACT server, nothing is uploaded |
10
+ | Preview | `minicube-dev preview --device ios-sim` (latest debug package) or `minicube-dev preview '<citysuper://…>'` | same as debug |
11
+ | Release | `minicube-dev release --publish full` | `minicube-dev release --publish full` |
12
+
13
+ The package contains **no** server addresses, environments or credentials. They all live in one
14
+ configuration file on each developer's machine.
15
+
16
+ ## Install
17
+
18
+ Requires Node.js 18.17+ (macOS or Windows).
19
+
20
+ ```bash
21
+ npm install -g @cityholdings/minicube-dev
22
+ ```
23
+
24
+ Upgrade with the same command. Offline: build a tarball with `npm run pack` (written to `dist/`)
25
+ and run `scripts/install.sh` (macOS) / `scripts\install.ps1` (Windows).
26
+
27
+ ### Publishing a new version (maintainers)
28
+
29
+ Copy `.npmrc.example` to `.npmrc` (git-ignored, never packed) and either set `NPM_TOKEN` or put
30
+ the token of the npm user `cityholdings` in place of `${NPM_TOKEN}`. Then:
31
+
32
+ ```bash
33
+ npm version patch
34
+ npm publish
35
+ ```
36
+
37
+ Optional tools, used only by the commands that need them: `adb` (Android preview), Xcode
38
+ command-line tools (`xcrun`, iOS simulator/device preview), `kubectl` (token administration or
39
+ `transport: "kube"`), `@antdigital/cube` in card projects, `cube-pack` (inline card images), and the
40
+ mini program compiler `mini-pkg-builder`.
41
+
42
+ ## Configuration
43
+
44
+ `~/.minicube-dev/config.json` (Windows: `%USERPROFILE%\.minicube-dev\config.json`), or the file named
45
+ by `MINICUBE_DEV_CONFIG` / `--config`. Create a skeleton with `minicube-dev config init`:
46
+
47
+ ```json
48
+ {
49
+ "defaultEnv": "tst",
50
+ "environments": {
51
+ "tst": {
52
+ "baseUrl": "https://<tst gateway host>/core",
53
+ "token": "mcd_…",
54
+ "previewScheme": "citysuper",
55
+ "kube": { "context": "<kubectl context>", "namespace": "<namespace>", "service": "<mpaas-core service>", "port": 8301 }
56
+ },
57
+ "uat": { "baseUrl": "https://<uat gateway host>/core", "tokenEnv": "MINICUBE_TOKEN_UAT" }
58
+ },
59
+ "devices": {
60
+ "android": { "package": "<android application id>", "cubeConnectActivity": "<debug activity accepting cubeActHost/cubeActPort>" },
61
+ "ios": { "bundleId": "<bundle id, physical devices only>" }
62
+ },
63
+ "builders": { "mini": "/path/to/mini-pkg-builder" }
64
+ }
65
+ ```
66
+
67
+ * `token` or `tokenEnv` (name of an environment variable holding it) per environment.
68
+ * Without `baseUrl` (or with `"transport": "kube"`) requests go through `kubectl port-forward`
69
+ using your local kubeconfig.
70
+ * `builders.mini` is the mini-pkg-builder executable (`mini-pkg-builder-macos-arm64`,
71
+ `mini-pkg-builder-win-x64.exe`, …) or its runtime `index.js`; `MINICUBE_MINI_BUILDER` overrides it.
72
+
73
+ `minicube-dev doctor` checks the toolchain and every environment's token.
74
+
75
+ ## Tokens
76
+
77
+ The CLI does not use the IDE username/password. Each environment issues tokens bound to one mPaaS
78
+ app + workspace (e.g. `<appId>/default`); a token can reach every mini program, H5
79
+ offline package and card of that app and nothing else.
80
+
81
+ * 256-bit random value (`mcd_` + 43 characters), only its SHA-256 is stored server side.
82
+ * Valid 365 days by default (max 730); `minicube-dev whoami` warns 30 days before expiry.
83
+ * Rotation issues a new token and keeps the old one valid for a grace period (default 7 days).
84
+
85
+ Administration needs kubectl access to the environment — the endpoints only answer requests
86
+ arriving on the pod's loopback interface, never through the public gateway:
87
+
88
+ ```bash
89
+ minicube-dev admin token create --env tst --name "ci" --app <appId> [--days 365] [--save]
90
+ minicube-dev admin token list --env tst
91
+ minicube-dev admin token rotate --env tst <id> [--grace 7] [--save]
92
+ minicube-dev admin token revoke --env tst <id>
93
+ ```
94
+
95
+ `--save` writes the new token into your config; otherwise hand it to the developer, who stores it
96
+ with `minicube-dev config set-token tst <token>`.
97
+
98
+ ## Project file
99
+
100
+ `minicube.json` at the project root (`minicube-dev init` writes one):
101
+
102
+ ```jsonc
103
+ // mini program (directory with app.json)
104
+ { "type": "mini", "resourceId": "2025081500000001", "debug": { "whitelist": "my-team" } }
105
+
106
+ // H5 offline package: build command and output directory become <id>/www
107
+ { "type": "h5", "resourceId": "70000000", "h5": { "buildCommand": "npm run build", "outputDir": "dist" },
108
+ "mainUrl": "/www/index.html", "debug": { "whitelist": "my-team" } }
109
+
110
+ // card (ACT v4 project)
111
+ { "type": "card", "resourceId": "register-info-consent", "card": { "dir": "./card" } }
112
+ ```
113
+
114
+ Optional: `version` (otherwise the next free version after the highest on the server; cards
115
+ default to `manifest.json`'s version), `mainUrl`, `clientScopes`
116
+ (`[{ "clientType": "iOS", "minVersion": "3.2.0" }]`, default iOS + Android without limits),
117
+ `enableTabBar`, `enableOptionMenu`, `expand`, `env`.
118
+
119
+ ## Whitelists
120
+
121
+ Debug packages are delivered only to members (client user ids) of their whitelists, and only when
122
+ opened through a preview link.
123
+
124
+ ```bash
125
+ minicube-dev whitelist list
126
+ minicube-dev whitelist create my-team --items 10001,10002
127
+ minicube-dev whitelist add <id> --items 10003
128
+ minicube-dev debug --whitelist my-team # or an id; repeatable
129
+ minicube-dev debug --whitelist-create qa --items 10001 # new whitelist for this upload
130
+ ```
131
+
132
+ ## Devices
133
+
134
+ `--device android | android:<serial> | ios-sim | ios-sim:<udid> | ios:<udid>`
135
+
136
+ * Mini program / H5 previews open `citysuper://platformapi/startapp?…` links (the mPaaS IDE
137
+ preview layout with the app scheme instead of `mpaas`). Only non-production builds accept them.
138
+ * Card previews attach the app to `act server` without scanning: Android starts the debug-only
139
+ connect activity (emulators get an `adb reverse` tunnel); the iOS simulator opens
140
+ `citysuper://cubedebug/connect?host=127.0.0.1&port=9001` (UAT target, which links CubeDev).
141
+ Physical phones scan the ACT QR code.
142
+
143
+ ## Release
144
+
145
+ ```bash
146
+ minicube-dev release --env tst # upload a release package (not yet visible)
147
+ minicube-dev publish <packageId> --env tst --mode full
148
+ minicube-dev release --env tst --publish full --yes # both in one go
149
+ minicube-dev publish <packageId> --mode whitelist --whitelist qa
150
+ minicube-dev publish <packageId> --mode timewindow --end 2026-11-30T00:00:00Z --count 500
151
+ ```
152
+
153
+ Full publishing asks for confirmation unless `--yes` is given.
154
+
155
+ ## Commands
156
+
157
+ Run `minicube-dev help`. `--json` prints machine-readable output; `MINICUBE_DEBUG=1` shows stack traces.
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+ import { printError } from '../src/log.js';
4
+
5
+ main(process.argv.slice(2)).then(
6
+ (code) => process.exit(code ?? 0),
7
+ (error) => {
8
+ printError(error);
9
+ process.exit(1);
10
+ },
11
+ );
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@cityholdings/minicube-dev",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Command-line workflow for mPaaS mini programs, H5 offline packages and Cube cards: build, debug upload, device preview and release",
5
+ "type": "module",
6
+ "bin": {
7
+ "minicube-dev": "bin/minicube-dev.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "scripts/install.sh",
13
+ "scripts/install.ps1",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18.17"
18
+ },
19
+ "scripts": {
20
+ "test": "node --test",
21
+ "pack": "npm pack --pack-destination dist",
22
+ "prepublishOnly": "node --test"
23
+ },
24
+ "license": "UNLICENSED",
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "registry": "https://registry.npmjs.org/"
28
+ }
29
+ }
@@ -0,0 +1,13 @@
1
+ # Install minicube-dev globally from the tarball next to this script (or the one given).
2
+ param([string]$Tarball)
3
+ $ErrorActionPreference = 'Stop'
4
+ if (-not $Tarball) {
5
+ $Tarball = Get-ChildItem (Join-Path $PSScriptRoot '..\dist\*minicube-dev-*.tgz') -ErrorAction SilentlyContinue | Sort-Object Name | Select-Object -Last 1 -ExpandProperty FullName
6
+ }
7
+ if (-not $Tarball -or -not (Test-Path $Tarball)) { throw "minicube-dev tarball not found; run 'npm run pack' first or pass -Tarball" }
8
+ $major = [int]((node -p "process.versions.node").Split('.')[0])
9
+ if ($major -lt 18) { throw 'Node.js 18.17+ is required' }
10
+ npm install -g $Tarball
11
+ minicube-dev --version
12
+ $config = if ($env:MINICUBE_DEV_CONFIG) { $env:MINICUBE_DEV_CONFIG } else { Join-Path $env:USERPROFILE '.minicube-dev\config.json' }
13
+ if (-not (Test-Path $config)) { Write-Host 'Next: minicube-dev config init' }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Install minicube-dev globally from the tarball next to this script (or the one given).
3
+ set -euo pipefail
4
+ tarball="${1:-$(ls "$(dirname "$0")"/../dist/*minicube-dev-*.tgz 2>/dev/null | tail -1)}"
5
+ [ -f "$tarball" ] || { echo "minicube-dev tarball not found; run 'npm run pack' first or pass its path"; exit 1; }
6
+ node -e 'process.exit(+process.versions.node.split(".")[0] >= 18 ? 0 : 1)' || { echo "Node.js 18.17+ is required"; exit 1; }
7
+ npm install -g "$tarball"
8
+ minicube-dev --version
9
+ [ -f "${MINICUBE_DEV_CONFIG:-$HOME/.minicube-dev/config.json}" ] || echo "Next: minicube-dev config init"
package/src/act.js ADDED
@@ -0,0 +1,150 @@
1
+ import { mkdirSync, openSync } from 'node:fs';
2
+ import { createConnection } from 'node:net';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { cardDir, resolveAct } from './build.js';
6
+ import { adb, androidSerial } from './device.js';
7
+ import { CliError, log } from './log.js';
8
+ import { delay, run, runSync, spawnDetached } from './proc.js';
9
+
10
+ const DEFAULT_PORT = 9001;
11
+ const CONNECT_TIMEOUT_MS = 45_000;
12
+ const SETTLE_MS = 2_000;
13
+
14
+ function isListening(port) {
15
+ return new Promise((resolve) => {
16
+ const socket = createConnection({ host: '127.0.0.1', port });
17
+ const done = (ok) => {
18
+ socket.destroy();
19
+ resolve(ok);
20
+ };
21
+ socket.setTimeout(1000);
22
+ socket.once('connect', () => done(true));
23
+ socket.once('error', () => done(false));
24
+ socket.once('timeout', () => done(false));
25
+ });
26
+ }
27
+
28
+ async function getJson(port, route) {
29
+ try {
30
+ const response = await fetch(`http://127.0.0.1:${port}${route}`, { signal: AbortSignal.timeout(3000) });
31
+ return response.ok ? await response.json() : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ export async function attachedDevices(port) {
38
+ const body = await getJson(port, '/api/device/list');
39
+ return Array.isArray(body?.data) ? body.data : [];
40
+ }
41
+
42
+ /**
43
+ * The address a device should use for the ACT server, which also ends up in every template file
44
+ * URL the server hands out. Tunnelled targets (Android over adb reverse, the iOS simulator) use
45
+ * loopback: it works behind on-device VPNs, which capture an app's LAN traffic.
46
+ */
47
+ export function advertisedHost(target) {
48
+ if (target?.kind === 'android' || target?.kind === 'ios-sim') return '127.0.0.1';
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * ACT v4 `act server` is left running so the device connection survives between previews. `host`
54
+ * is the address it advertises (HOST env of the ACT server); a running server advertising another
55
+ * address is restarted.
56
+ */
57
+ export async function ensureActServer(project, port = DEFAULT_PORT, host = null) {
58
+ const act = resolveAct(project.root);
59
+ if (!act) throw new CliError('ACT not found', 'Install it in the card project: npm i -D @antdigital/cube');
60
+ if (await isListening(port)) {
61
+ const current = (await serverAddress(port)).host;
62
+ if (!host || current === host) {
63
+ log.detail(`Reusing the ACT server on port ${port} (${current})`);
64
+ return act;
65
+ }
66
+ log.detail(`Restarting the ACT server: it advertises ${current}, this device needs ${host}`);
67
+ await run(act, ['prepare'], { cwd: project.root, capture: true });
68
+ const freed = Date.now() + 10_000;
69
+ while ((await isListening(port)) && Date.now() < freed) await delay(300);
70
+ }
71
+ mkdirSync(path.join(project.root, '.minicube'), { recursive: true });
72
+ const logFile = path.join(project.root, '.minicube', 'act-server.log');
73
+ spawnDetached(act, ['server'], { cwd: project.root, logFd: openSync(logFile, 'a'), env: host ? { HOST: host } : undefined });
74
+ const deadline = Date.now() + 20_000;
75
+ while (Date.now() < deadline) {
76
+ await delay(500);
77
+ if (await isListening(port)) {
78
+ log.detail(`ACT server started on port ${port} (log: ${path.relative(project.root, logFile)})`);
79
+ return act;
80
+ }
81
+ }
82
+ throw new CliError(`ACT server did not start on port ${port}`, `See ${logFile}; free the port with \`act prepare\`.`);
83
+ }
84
+
85
+ function lanAddress() {
86
+ for (const addresses of Object.values(os.networkInterfaces())) {
87
+ for (const a of addresses ?? []) if (a.family === 'IPv4' && !a.internal) return a.address;
88
+ }
89
+ return '127.0.0.1';
90
+ }
91
+
92
+ /** host/port the server advertises in its QR code, e.g. alipays://...?ip=192.168.1.5&port=9001 */
93
+ async function serverAddress(port) {
94
+ const body = await getJson(port, '/api/server/qrvalue');
95
+ const value = typeof body?.data === 'string' ? body.data : '';
96
+ const match = value.match(/[?&](?:ip|host)=([^&\s]+)[\s\S]*?[?&]port=(\d{2,5})/);
97
+ return match ? { host: decodeURIComponent(match[1]), port: match[2] } : { host: lanAddress(), port: String(port) };
98
+ }
99
+
100
+ /**
101
+ * Attach the device to the ACT server without scanning:
102
+ * - Android: start the debug-only connect activity with cubeActHost/cubeActPort extras, over an
103
+ * adb reverse tunnel to 127.0.0.1 (USB devices and emulators alike)
104
+ * - iOS simulator: open <scheme>://cubedebug/connect?host=&port= (UAT builds with CubeDev)
105
+ */
106
+ export async function attachDevice(target, env, devices, port = DEFAULT_PORT) {
107
+ const before = (await attachedDevices(port)).length;
108
+ const address = await serverAddress(port);
109
+ if (target.kind === 'android') {
110
+ const serial = androidSerial(target);
111
+ const pkg = devices.android?.package;
112
+ const activity = devices.android?.cubeConnectActivity;
113
+ if (!pkg || !activity || pkg.includes('<') || activity.includes('<')) {
114
+ throw new CliError('devices.android.package and devices.android.cubeConnectActivity must be configured for card preview');
115
+ }
116
+ adb(serial, ['reverse', `tcp:${address.port}`, `tcp:${address.port}`]);
117
+ const host = '127.0.0.1';
118
+ adb(serial, ['shell', 'am', 'force-stop', pkg]);
119
+ const component = activity.startsWith('.') ? `${pkg}/${pkg}${activity}` : `${pkg}/${activity}`;
120
+ adb(serial, ['shell', 'am', 'start', '-n', component, '--es', 'cubeActHost', host, '--es', 'cubeActPort', address.port]);
121
+ log.detail(`Android ${serial} -> ${host}:${address.port}`);
122
+ } else if (target.kind === 'ios-sim') {
123
+ const scheme = env?.previewScheme ?? 'citysuper';
124
+ // The simulator shares the Mac's network stack, so loopback reaches the ACT server.
125
+ const url = `${scheme}://cubedebug/connect?host=127.0.0.1&port=${address.port}`;
126
+ const result = runSync('xcrun', ['simctl', 'openurl', target.id, url]);
127
+ if (result.code !== 0) throw new CliError(`simctl openurl failed: ${result.stderr.trim()}`);
128
+ log.detail(`iOS simulator -> 127.0.0.1:${address.port}`);
129
+ } else {
130
+ log.info(`Scan the ACT QR code from the app, or connect the phone to ${address.host}:${address.port}`);
131
+ }
132
+ const deadline = Date.now() + CONNECT_TIMEOUT_MS;
133
+ while (Date.now() < deadline) {
134
+ if ((await attachedDevices(port)).length > before) {
135
+ // The websocket reports attached before the host's preview bridge is ready on a cold start.
136
+ await delay(SETTLE_MS);
137
+ return;
138
+ }
139
+ await delay(1000);
140
+ }
141
+ if ((await attachedDevices(port)).length === 0) {
142
+ throw new CliError('The app did not attach to the ACT server', 'The host app must be a debug/UAT build with Cube debugging enabled, and the device must reach this machine.');
143
+ }
144
+ log.warn('No new attachment observed; delivering to the devices already attached');
145
+ }
146
+
147
+ export async function deliverCard(project, act) {
148
+ const result = await run(act, ['preview', cardDir(project)], { cwd: project.root });
149
+ if (result.code !== 0) throw new CliError(`act preview failed (exit ${result.code})`);
150
+ }
package/src/api.js ADDED
@@ -0,0 +1,138 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { CliError } from './log.js';
4
+ import { openPortForward } from './kube.js';
5
+
6
+ export const TOKEN_HEADER = 'X-Minicube-Token';
7
+
8
+ /**
9
+ * Client for the mpaas-core `/cli/v1` API. The environment reaches the server either through its
10
+ * public `baseUrl` or, with `transport: "kube"` (or no baseUrl), through a kubectl port-forward.
11
+ */
12
+ export async function connect(env) {
13
+ if (!env.token) {
14
+ throw new CliError(`No token configured for environment "${env.name}"`, 'Ask an administrator for a token (minicube-dev admin token create) and put it in the config as "token" or "tokenEnv".');
15
+ }
16
+ let baseUrl = env.baseUrl && !String(env.baseUrl).includes('<') ? String(env.baseUrl).replace(/\/+$/, '') : null;
17
+ let tunnel = null;
18
+ if (env.transport === 'kube' || !baseUrl) {
19
+ tunnel = await openPortForward(env.kube);
20
+ baseUrl = tunnel.baseUrl;
21
+ }
22
+ return new ApiClient(`${baseUrl}/cli/v1`, env.token, () => tunnel?.close());
23
+ }
24
+
25
+ export class ApiClient {
26
+ constructor(root, token, onClose) {
27
+ this.root = root;
28
+ this.token = token;
29
+ this.onClose = onClose;
30
+ }
31
+
32
+ close() {
33
+ this.onClose?.();
34
+ }
35
+
36
+ async request(method, route, { json, form, query } = {}) {
37
+ const url = new URL(this.root + route);
38
+ for (const [k, v] of Object.entries(query ?? {})) if (v !== undefined && v !== null) url.searchParams.set(k, String(v));
39
+ const headers = { [TOKEN_HEADER]: this.token, Accept: 'application/json' };
40
+ let body;
41
+ if (json !== undefined) {
42
+ headers['Content-Type'] = 'application/json';
43
+ body = JSON.stringify(json);
44
+ } else if (form) {
45
+ body = form;
46
+ }
47
+ let response;
48
+ try {
49
+ response = await fetch(url, { method, headers, body, signal: AbortSignal.timeout(10 * 60_000) });
50
+ } catch (error) {
51
+ throw new CliError(`Request to ${url.origin} failed: ${error.cause?.message ?? error.message}`);
52
+ }
53
+ const text = await response.text();
54
+ let payload;
55
+ try {
56
+ payload = JSON.parse(text);
57
+ } catch {
58
+ throw new CliError(`Unexpected response from server (HTTP ${response.status}): ${text.slice(0, 300)}`);
59
+ }
60
+ if (response.status === 401 || payload.code === 4010) {
61
+ throw new CliError('The server rejected the CLI token (invalid, expired or revoked)', 'Check `minicube-dev token status`, or ask an administrator to rotate it.');
62
+ }
63
+ if (payload.code !== 1000) {
64
+ throw new CliError(`Server error ${payload.code ?? response.status}: ${payload.msg ?? payload.error ?? text.slice(0, 300)}`);
65
+ }
66
+ return payload.data;
67
+ }
68
+
69
+ whoami() {
70
+ return this.request('GET', '/whoami');
71
+ }
72
+ resources(type) {
73
+ return this.request('GET', '/resources', { query: { type } });
74
+ }
75
+ resource(id) {
76
+ return this.request('GET', `/resources/${encodeURIComponent(id)}`);
77
+ }
78
+ packages(resourceId, limit = 20) {
79
+ return this.request('GET', `/resources/${encodeURIComponent(resourceId)}/packages`, { query: { limit } });
80
+ }
81
+ pkg(id) {
82
+ return this.request('GET', `/packages/${id}`);
83
+ }
84
+ upload(meta, file) {
85
+ const form = new FormData();
86
+ form.set('meta', JSON.stringify(meta));
87
+ form.set('file', new Blob([readFileSync(file)], { type: 'application/zip' }), path.basename(file));
88
+ return this.request('POST', '/packages', { form });
89
+ }
90
+ publish(id, body) {
91
+ return this.request('POST', `/packages/${id}/publish`, { json: body });
92
+ }
93
+ whitelists() {
94
+ return this.request('GET', '/whitelists');
95
+ }
96
+ whitelist(id) {
97
+ return this.request('GET', `/whitelists/${id}`);
98
+ }
99
+ createWhitelist(name, items) {
100
+ return this.request('POST', '/whitelists', { json: { name, items } });
101
+ }
102
+ addWhitelistItems(id, items) {
103
+ return this.request('POST', `/whitelists/${id}/items`, { json: { items } });
104
+ }
105
+ removeWhitelistItems(id, items) {
106
+ return this.request('POST', `/whitelists/${id}/items/remove`, { json: { items } });
107
+ }
108
+ }
109
+
110
+ /** Admin client: token management through a kubectl port-forward only. */
111
+ export async function connectAdmin(env) {
112
+ const tunnel = await openPortForward(env.kube);
113
+ const root = `${tunnel.baseUrl}/cli/admin/tokens`;
114
+ const call = async (method, route = '', json) => {
115
+ const response = await fetch(root + route, {
116
+ method,
117
+ headers: json ? { 'Content-Type': 'application/json' } : {},
118
+ body: json ? JSON.stringify(json) : undefined,
119
+ signal: AbortSignal.timeout(60_000),
120
+ });
121
+ const text = await response.text();
122
+ let payload;
123
+ try {
124
+ payload = JSON.parse(text);
125
+ } catch {
126
+ throw new CliError(`Unexpected admin response (HTTP ${response.status}): ${text.slice(0, 300)}`, 'Is the minicube-dev backend deployed to this environment?');
127
+ }
128
+ if (payload.code !== 1000) throw new CliError(`Server error ${payload.code}: ${payload.msg}`);
129
+ return payload.data;
130
+ };
131
+ return {
132
+ list: (appId) => call('GET', appId ? `?appId=${encodeURIComponent(appId)}` : ''),
133
+ issue: (body) => call('POST', '', body),
134
+ rotate: (id, body) => call('POST', `/${id}/rotate`, body),
135
+ revoke: (id, body) => call('POST', `/${id}/revoke`, body),
136
+ close: () => tunnel.close(),
137
+ };
138
+ }
package/src/args.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Minimal argv parser: positionals, `--flag`, `--key value`, `--key=value`, `-k value`, `--no-flag`.
3
+ * Repeated keys collect into arrays.
4
+ */
5
+ const ALIASES = { e: 'env', c: 'config', d: 'device', v: 'version', w: 'whitelist', y: 'yes', h: 'help', t: 'type', p: 'project' };
6
+ const BOOLEAN = new Set(['help', 'yes', 'json', 'quiet', 'no-build', 'open', 'remote', 'save', 'force', 'skip-build', 'dry-run']);
7
+
8
+ export function parseArgs(argv) {
9
+ const positionals = [];
10
+ const flags = {};
11
+ const set = (key, value) => {
12
+ if (key in flags) flags[key] = [].concat(flags[key], value);
13
+ else flags[key] = value;
14
+ };
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const arg = argv[i];
17
+ if (arg === '--') {
18
+ positionals.push(...argv.slice(i + 1));
19
+ break;
20
+ }
21
+ if (arg.startsWith('--')) {
22
+ const eq = arg.indexOf('=');
23
+ let key = eq > 0 ? arg.slice(2, eq) : arg.slice(2);
24
+ if (eq > 0) {
25
+ set(key, arg.slice(eq + 1));
26
+ } else if (key.startsWith('no-') && !BOOLEAN.has(key)) {
27
+ set(key.slice(3), false);
28
+ } else if (BOOLEAN.has(key) || i + 1 >= argv.length || argv[i + 1].startsWith('-')) {
29
+ set(key, true);
30
+ } else {
31
+ set(key, argv[++i]);
32
+ }
33
+ } else if (arg.startsWith('-') && arg.length === 2) {
34
+ const key = ALIASES[arg[1]] ?? arg[1];
35
+ if (BOOLEAN.has(key) || i + 1 >= argv.length || argv[i + 1].startsWith('-')) set(key, true);
36
+ else set(key, argv[++i]);
37
+ } else {
38
+ positionals.push(arg);
39
+ }
40
+ }
41
+ return { positionals, flags };
42
+ }
43
+
44
+ /** Last value of a flag that may have been repeated. */
45
+ export function one(value) {
46
+ return Array.isArray(value) ? value[value.length - 1] : value;
47
+ }
48
+
49
+ /** Comma separated and/or repeated flag values as a flat list. */
50
+ export function list(value) {
51
+ if (value === undefined || value === true || value === false) return [];
52
+ return [].concat(value).flatMap((v) => String(v).split(',')).map((v) => v.trim()).filter(Boolean);
53
+ }