@hypersoniclabs/helix-cli 0.1.0

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 ADDED
@@ -0,0 +1,61 @@
1
+ # @hypersoniclabs/helix-cli
2
+
3
+ `helix` — the **HELIX creator CLI**: scaffold, validate, and publish Instant Worlds to the HELIX
4
+ backend. It shares bundle validation with the server via `@hypersoniclabs/helix-manifest`, so "validate passed but
5
+ publish failed" can't happen for contract reasons.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @hypersoniclabs/helix-cli
11
+ ```
12
+
13
+ ## Authentication
14
+
15
+ The CLI authenticates with a **client token** (a long-lived API token tied to your account).
16
+
17
+ 1. Generate one in the **Creator Hub** → *Settings → Client Tokens*.
18
+ 2. Run `helix login` and paste it (or `helix login --token <token>`).
19
+
20
+ The token is stored in `~/.helix/credentials.json` (mode `600`). It's sent as
21
+ `Authorization: Bearer <token>` on publish calls. `helix login` validates it against
22
+ `GET /api/v1/users/me` before saving and reads the token's `exp` so the CLI knows when it lapses.
23
+
24
+ > Point the CLI at a backend with `--api-url` or the `HELIX_API_URL` env var
25
+ > (default `http://localhost:5000`).
26
+
27
+ ## Commands
28
+
29
+ | Command | What it does |
30
+ |---|---|
31
+ | `helix login [--api-url <url>] [--token <t>]` | Store a client token for the CLI. |
32
+ | `helix logout` | Remove the stored token. |
33
+ | `helix whoami` | Show current login status. |
34
+ | `helix init <dir>` | Scaffold a minimal Three.js world (`helix.json` + `index.html` + `main.js`). |
35
+ | `helix validate [dir]` | Validate a bundle locally — the exact rules the server enforces. |
36
+ | `helix publish [dir]` | Validate → resolve/create the world → upload files → finalize → print the play URL. |
37
+ | `helix list` | List your worlds. |
38
+
39
+ ## Publish flow
40
+
41
+ `helix publish` maps directly onto the backend's Instant World API:
42
+
43
+ 1. `checkBundle` validates `helix.json` + files via `@hypersoniclabs/helix-manifest`.
44
+ 2. `GET /api/v1/instant-worlds/me` → reuse the world with the manifest's slug, else
45
+ `POST /api/v1/instant-worlds`.
46
+ 3. `POST /api/v1/instant-worlds/:id/builds` → presigned upload tickets.
47
+ 4. Files PUT directly to storage (6 concurrent), content-type/length bound to each ticket.
48
+ 5. `POST /api/v1/instant-worlds/:id/builds/:buildId/finalize` → byte-exact verify, activate, publish.
49
+ 6. `GET /api/v1/instant-worlds/:slug` → the play URL.
50
+
51
+ The programmatic surface (`publishWorld`, `checkBundle`, `whoAmI`, …) is exported from the package
52
+ main, so an MCP server can drive the exact same code paths an agent would.
53
+
54
+ ## Develop
55
+
56
+ ```bash
57
+ npm install
58
+ npm test # jest
59
+ npm run build # tsc → dist/ (bin: dist/index.js)
60
+ npm run lint
61
+ ```
@@ -0,0 +1,73 @@
1
+ export type ManifestIssue = {
2
+ path: string;
3
+ message: string;
4
+ };
5
+ export type ParsedSemver = {
6
+ major: number;
7
+ minor: number;
8
+ patch: number;
9
+ prerelease: string[];
10
+ };
11
+ export type AbilityValidator = {
12
+ validateAbilityManifest(value: unknown): ManifestIssue[];
13
+ /** Present on the real vendored artifact — enables the install-time system-pin check (gate 2). */
14
+ satisfiesSystemPin?(systemVersion: string, range: string): boolean;
15
+ /** Frozen semver primitives — the single source of `^`/`~` + prerelease ordering (reused by resolvePins). */
16
+ parseSemver?(version: string): ParsedSemver | null;
17
+ compareSemver?(a: ParsedSemver, b: ParsedSemver): number;
18
+ SYSTEM_IDENTITY?: {
19
+ id: string;
20
+ version: string;
21
+ skeleton: string;
22
+ };
23
+ };
24
+ export type InstalledIndexValidator = {
25
+ validateInstalledIndex(value: unknown): ManifestIssue[];
26
+ isResolvedEntry(entry: Record<string, unknown>): boolean;
27
+ };
28
+ export type AbilityManifestLike = {
29
+ id: string;
30
+ version: string;
31
+ title: string;
32
+ system: {
33
+ id: string;
34
+ version: string;
35
+ skeleton: string;
36
+ };
37
+ activation: {
38
+ mode: string;
39
+ };
40
+ entry: string;
41
+ configSchema: string;
42
+ clips?: {
43
+ name: string;
44
+ src: string;
45
+ }[];
46
+ };
47
+ export declare function loadVendoredValidator(): Promise<AbilityValidator>;
48
+ export declare function loadInstalledIndexValidator(): Promise<InstalledIndexValidator>;
49
+ export type AbilityBundleCheck = {
50
+ manifest: AbilityManifestLike | null;
51
+ errors: string[];
52
+ };
53
+ /** True when an ability's built entry has three.js bundled in rather than left external. */
54
+ export declare function bundlesThree(entrySource: string): boolean;
55
+ /** Validates an ability bundle directory: ability.json against the frozen contract + referenced files exist. */
56
+ export declare function checkAbilityBundle(root: string, validator: AbilityValidator): AbilityBundleCheck;
57
+ export declare const MODULES_DIR = "helix_modules";
58
+ export declare const INSTALLED_INDEX = "installed.json";
59
+ export type InstallResult = {
60
+ id: string;
61
+ version: string;
62
+ /** Bundle path relative to the index (== the ability id). */
63
+ path: string;
64
+ /** Total entries in installed.json after this install. */
65
+ installedCount: number;
66
+ };
67
+ /**
68
+ * `helix install <bundle> --first-party` (a.k.a. `--local`): the FIRST-PARTY whole-bundle copy —
69
+ * validates, copies the bundle (code AND clips) into `<worldDir>/helix_modules/<id>/`, and upserts a
70
+ * v1 local `installed.json` entry. Used by the engine devs to test co-located assets; consumer
71
+ * worlds use `helix install` (no path) which is code-only with assets streaming from the CDN.
72
+ */
73
+ export declare function installLocalAbility(bundleDir: string, worldDir: string, validator: AbilityValidator, indexValidator: InstalledIndexValidator): InstallResult;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INSTALLED_INDEX = exports.MODULES_DIR = void 0;
4
+ exports.loadVendoredValidator = loadVendoredValidator;
5
+ exports.loadInstalledIndexValidator = loadInstalledIndexValidator;
6
+ exports.bundlesThree = bundlesThree;
7
+ exports.checkAbilityBundle = checkAbilityBundle;
8
+ exports.installLocalAbility = installLocalAbility;
9
+ const node_fs_1 = require("node:fs");
10
+ const node_path_1 = require("node:path");
11
+ const node_url_1 = require("node:url");
12
+ // The vendor file is ESM; this package compiles to CommonJS, where tsc downlevels `import()` to
13
+ // `require()` — which cannot load ESM. The Function indirection keeps the native dynamic import.
14
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
15
+ async function loadVendoredValidator() {
16
+ const url = (0, node_url_1.pathToFileURL)((0, node_path_1.join)(__dirname, '..', 'vendor', 'ability-manifest.mjs')).href;
17
+ return (await dynamicImport(url));
18
+ }
19
+ async function loadInstalledIndexValidator() {
20
+ const url = (0, node_url_1.pathToFileURL)((0, node_path_1.join)(__dirname, '..', 'vendor', 'installed-index.mjs')).href;
21
+ return (await dynamicImport(url));
22
+ }
23
+ // three's bundled source carries its REVISION constant (`const REVISION = '172'`) and core class
24
+ // declarations; an external bare import (`import ... from 'three'`) produces only property access
25
+ // against the namespace (`THREE.Vector3`), never these definition forms. Either signal therefore
26
+ // means three got inlined — which forks the instance and silently breaks instanceof across the host.
27
+ const THREE_REVISION = /\bREVISION\s*=\s*['"]\d+['"]/;
28
+ const THREE_CORE_CLASS = /\bclass\s+(Object3D|BufferGeometry|Matrix4|Vector3|Quaternion|Euler|PerspectiveCamera|WebGLRenderer|Material)\b/g;
29
+ /** True when an ability's built entry has three.js bundled in rather than left external. */
30
+ function bundlesThree(entrySource) {
31
+ if (THREE_REVISION.test(entrySource))
32
+ return true;
33
+ const classes = new Set(entrySource.match(THREE_CORE_CLASS) ?? []);
34
+ return classes.size >= 2;
35
+ }
36
+ /** Validates an ability bundle directory: ability.json against the frozen contract + referenced files exist. */
37
+ function checkAbilityBundle(root, validator) {
38
+ const manifestPath = (0, node_path_1.join)(root, 'ability.json');
39
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
40
+ const hint = (0, node_fs_1.existsSync)((0, node_path_1.join)(root, 'helix.json'))
41
+ ? 'this looks like a WORLD bundle (helix.json) — use `helix validate`'
42
+ : 'is this the built bundle directory (dist/)?';
43
+ return { manifest: null, errors: [`no ability.json at the bundle root — ${hint}`] };
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
48
+ }
49
+ catch (err) {
50
+ return { manifest: null, errors: [`ability.json is not valid JSON: ${err instanceof Error ? err.message : err}`] };
51
+ }
52
+ const issues = validator.validateAbilityManifest(parsed);
53
+ if (issues.length) {
54
+ return { manifest: null, errors: issues.map((i) => `ability.json ${i.path === '' ? '(root)' : i.path}: ${i.message}`) };
55
+ }
56
+ const manifest = parsed;
57
+ const errors = [];
58
+ // System-pin check against the vendored validator's own system identity (gate 2: refused at
59
+ // install, not just at load) — the artifact banner records which system version it came from.
60
+ const system = validator.SYSTEM_IDENTITY;
61
+ if (system !== undefined && validator.satisfiesSystemPin !== undefined) {
62
+ if (manifest.system.id !== system.id) {
63
+ errors.push(`system.id '${manifest.system.id}' does not match the running system '${system.id}'`);
64
+ }
65
+ else if (!validator.satisfiesSystemPin(system.version, manifest.system.version)) {
66
+ errors.push(`system.version '${manifest.system.version}' is not satisfied by the system version ${system.version}`);
67
+ }
68
+ else if (manifest.system.skeleton !== system.skeleton) {
69
+ errors.push(`system.skeleton '${manifest.system.skeleton}' does not match the running skeleton standard '${system.skeleton}'`);
70
+ }
71
+ }
72
+ const referenced = [manifest.entry, manifest.configSchema, ...(manifest.clips ?? []).map((c) => c.src)];
73
+ for (const rel of referenced) {
74
+ if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(root, rel)))
75
+ errors.push(`referenced file missing from the bundle: ${rel}`);
76
+ }
77
+ // Single-instance invariant: the world supplies the one shared three; the entry must import it,
78
+ // not ship its own copy. Build the ability with `--external three`.
79
+ const entryPath = (0, node_path_1.join)(root, manifest.entry);
80
+ if ((0, node_fs_1.existsSync)(entryPath) && bundlesThree((0, node_fs_1.readFileSync)(entryPath, 'utf8'))) {
81
+ errors.push(`${manifest.entry} inlines three.js — rebuild the ability with \`--external three\` so the world supplies the single shared instance`);
82
+ }
83
+ return { manifest: errors.length ? null : manifest, errors };
84
+ }
85
+ exports.MODULES_DIR = 'helix_modules';
86
+ exports.INSTALLED_INDEX = 'installed.json';
87
+ /**
88
+ * `helix install <bundle> --first-party` (a.k.a. `--local`): the FIRST-PARTY whole-bundle copy —
89
+ * validates, copies the bundle (code AND clips) into `<worldDir>/helix_modules/<id>/`, and upserts a
90
+ * v1 local `installed.json` entry. Used by the engine devs to test co-located assets; consumer
91
+ * worlds use `helix install` (no path) which is code-only with assets streaming from the CDN.
92
+ */
93
+ function installLocalAbility(bundleDir, worldDir, validator, indexValidator) {
94
+ const check = checkAbilityBundle(bundleDir, validator);
95
+ if (check.errors.length || check.manifest === null) {
96
+ throw new Error(`bundle refused:\n${check.errors.map((e) => ` - ${e}`).join('\n')}`);
97
+ }
98
+ const { id, version } = check.manifest;
99
+ const modulesDir = (0, node_path_1.join)(worldDir, exports.MODULES_DIR);
100
+ const indexPath = (0, node_path_1.join)(modulesDir, exports.INSTALLED_INDEX);
101
+ let index = [];
102
+ if ((0, node_fs_1.existsSync)(indexPath)) {
103
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(indexPath, 'utf8'));
104
+ const issues = indexValidator.validateInstalledIndex(parsed);
105
+ if (issues.length) {
106
+ throw new Error(`existing ${exports.INSTALLED_INDEX} is invalid (delete it to regenerate):\n${issues.map((i) => ` ${i.path}: ${i.message}`).join('\n')}`);
107
+ }
108
+ index = parsed;
109
+ }
110
+ const target = (0, node_path_1.join)(modulesDir, id);
111
+ (0, node_fs_1.rmSync)(target, { recursive: true, force: true });
112
+ (0, node_fs_1.mkdirSync)(target, { recursive: true });
113
+ (0, node_fs_1.cpSync)(bundleDir, target, { recursive: true });
114
+ index = index.filter((entry) => entry.id !== id);
115
+ index.push({ id, version, path: id });
116
+ index.sort((a, b) => a.id.localeCompare(b.id));
117
+ (0, node_fs_1.writeFileSync)(indexPath, `${JSON.stringify(index, null, 2)}\n`);
118
+ return { id, version, path: id, installedCount: index.length };
119
+ }
120
+ //# sourceMappingURL=ability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ability.js","sourceRoot":"","sources":["../src/ability.ts"],"names":[],"mappings":"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"}
package/dist/api.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { CliCredentials } from './config';
2
+ type NestErrorBody = {
3
+ statusCode?: number;
4
+ message?: string | string[];
5
+ error?: string;
6
+ };
7
+ export declare class ApiError extends Error {
8
+ readonly status: number;
9
+ readonly body: NestErrorBody;
10
+ constructor(status: number, body: NestErrorBody);
11
+ private static messages;
12
+ render(): string;
13
+ }
14
+ export declare class HelixApi {
15
+ private readonly baseUrl;
16
+ private token;
17
+ constructor(baseUrl: string, token?: string | null);
18
+ setToken(token: string): void;
19
+ request<T>(method: string, path: string, body?: unknown): Promise<T>;
20
+ requestItem<T>(method: string, path: string, body?: unknown): Promise<T>;
21
+ static forCredentials(creds: CliCredentials): HelixApi;
22
+ }
23
+ export declare function uploadToTicket(url: string, body: Buffer, contentType: string, cacheControl?: string): Promise<void>;
24
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HelixApi = exports.ApiError = void 0;
4
+ exports.uploadToTicket = uploadToTicket;
5
+ class ApiError extends Error {
6
+ status;
7
+ body;
8
+ constructor(status, body) {
9
+ super(ApiError.messages(body).join('; ') || `HTTP ${status}`);
10
+ this.status = status;
11
+ this.body = body;
12
+ }
13
+ static messages(body) {
14
+ if (Array.isArray(body.message))
15
+ return body.message;
16
+ if (typeof body.message === 'string')
17
+ return [body.message];
18
+ return [];
19
+ }
20
+ // Rendered for terminal humans.
21
+ render() {
22
+ const heading = this.body.error ?? 'Error';
23
+ const lines = [`✖ ${heading} (${this.status})`];
24
+ for (const msg of ApiError.messages(this.body))
25
+ lines.push(` - ${msg}`);
26
+ return lines.join('\n');
27
+ }
28
+ }
29
+ exports.ApiError = ApiError;
30
+ class HelixApi {
31
+ baseUrl;
32
+ token;
33
+ constructor(baseUrl, token = null) {
34
+ this.baseUrl = baseUrl;
35
+ this.token = token;
36
+ }
37
+ setToken(token) {
38
+ this.token = token;
39
+ }
40
+ async request(method, path, body) {
41
+ const res = await fetch(`${this.baseUrl}${path}`, {
42
+ method,
43
+ headers: {
44
+ 'content-type': 'application/json',
45
+ ...(this.token ? { authorization: `Bearer ${this.token}` } : {}),
46
+ },
47
+ body: body === undefined ? undefined : JSON.stringify(body),
48
+ });
49
+ if (!res.ok) {
50
+ const problem = (await res.json().catch(() => ({})));
51
+ throw new ApiError(res.status, problem);
52
+ }
53
+ if (res.status === 204)
54
+ return undefined;
55
+ return (await res.json());
56
+ }
57
+ // Convenience for endpoints that return a SingleResponseDto ({ item: T }).
58
+ async requestItem(method, path, body) {
59
+ const envelope = await this.request(method, path, body);
60
+ return envelope.item;
61
+ }
62
+ static forCredentials(creds) {
63
+ return new HelixApi(creds.apiUrl, creds.token);
64
+ }
65
+ }
66
+ exports.HelixApi = HelixApi;
67
+ // Uploads a file body to a presigned URL. Content-Type (and, for package
68
+ // uploads, Cache-Control) are part of the signature, so the client must echo
69
+ // them exactly or R2 rejects the PUT with a signature mismatch.
70
+ async function uploadToTicket(url, body, contentType, cacheControl) {
71
+ const res = await fetch(url, {
72
+ method: 'PUT',
73
+ body: new Uint8Array(body),
74
+ headers: {
75
+ 'content-type': contentType,
76
+ ...(cacheControl ? { 'cache-control': cacheControl } : {}),
77
+ },
78
+ });
79
+ if (!res.ok) {
80
+ throw new Error(`upload failed (${res.status}): ${await res.text().catch(() => '')}`);
81
+ }
82
+ }
83
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { NormalizedHelixManifest } from '@hypersoniclabs/helix-manifest';
2
+ export type BundleFile = {
3
+ path: string;
4
+ size: number;
5
+ absolutePath: string;
6
+ };
7
+ export type BundleCheck = {
8
+ manifest: NormalizedHelixManifest | null;
9
+ files: BundleFile[];
10
+ errors: string[];
11
+ };
12
+ export declare function checkBundle(root: string): BundleCheck;
package/dist/bundle.js ADDED
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkBundle = checkBundle;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const helix_manifest_1 = require("@hypersoniclabs/helix-manifest");
7
+ const IGNORED_DIRS = new Set(['node_modules', '.git', '.vscode', '.idea']);
8
+ function walk(dir, root, out) {
9
+ for (const name of (0, node_fs_1.readdirSync)(dir)) {
10
+ if (IGNORED_DIRS.has(name))
11
+ continue;
12
+ const absolutePath = (0, node_path_1.join)(dir, name);
13
+ const stat = (0, node_fs_1.statSync)(absolutePath);
14
+ if (stat.isDirectory()) {
15
+ walk(absolutePath, root, out);
16
+ }
17
+ else {
18
+ out.push({ path: (0, node_path_1.relative)(root, absolutePath).replaceAll('\\', '/'), size: stat.size, absolutePath });
19
+ }
20
+ }
21
+ }
22
+ // Local validation with the exact same rules the server enforces at publish —
23
+ // shared via @hypersoniclabs/helix-manifest, so "validate passed but publish failed" can't
24
+ // happen for contract reasons.
25
+ function checkBundle(root) {
26
+ const errors = [];
27
+ const files = [];
28
+ try {
29
+ walk(root, root, files);
30
+ }
31
+ catch (err) {
32
+ return { manifest: null, files: [], errors: [`cannot read directory: ${err instanceof Error ? err.message : err}`] };
33
+ }
34
+ const manifestFile = files.find((f) => f.path === helix_manifest_1.MANIFEST_FILENAME);
35
+ let manifest = null;
36
+ if (!manifestFile) {
37
+ errors.push(`no ${helix_manifest_1.MANIFEST_FILENAME} found at the bundle root`);
38
+ }
39
+ else {
40
+ const result = (0, helix_manifest_1.parseManifest)((0, node_fs_1.readFileSync)(manifestFile.absolutePath, 'utf8'));
41
+ if (result.valid)
42
+ manifest = result.manifest;
43
+ else
44
+ errors.push(...result.errors);
45
+ }
46
+ if (files.length > helix_manifest_1.BUNDLE_MAX_FILES)
47
+ errors.push(`${files.length} files — limit is ${helix_manifest_1.BUNDLE_MAX_FILES}`);
48
+ let total = 0;
49
+ for (const file of files) {
50
+ total += file.size;
51
+ if (!helix_manifest_1.BUNDLE_PATH_PATTERN.test(file.path))
52
+ errors.push(`invalid path: ${file.path}`);
53
+ else if ((0, helix_manifest_1.bundleContentType)(file.path) === null)
54
+ errors.push(`file type not allowed: ${file.path}`);
55
+ if (file.size > helix_manifest_1.BUNDLE_MAX_FILE_BYTES)
56
+ errors.push(`${file.path} is ${file.size} bytes — per-file limit is ${helix_manifest_1.BUNDLE_MAX_FILE_BYTES}`);
57
+ if (file.size === 0)
58
+ errors.push(`${file.path} is empty`);
59
+ }
60
+ if (total > helix_manifest_1.BUNDLE_MAX_TOTAL_BYTES)
61
+ errors.push(`bundle is ${total} bytes — limit is ${helix_manifest_1.BUNDLE_MAX_TOTAL_BYTES}`);
62
+ if (manifest && !files.some((f) => f.path === manifest.entry)) {
63
+ errors.push(`entry "${manifest.entry}" does not exist in the bundle`);
64
+ }
65
+ // v0.2 worlds embed system/ability CODE under helix_modules/ but never HELIX assets —
66
+ // those stream from the CDN. A clip/texture under helix_modules/ is a licensing leak.
67
+ if (manifest?.helixVersion === '0.2') {
68
+ const leaked = (0, helix_manifest_1.embeddedModuleAssets)(files.map((f) => f.path));
69
+ if (leaked.length) {
70
+ errors.push(`HELIX assets must stream from the CDN, not ship under helix_modules/: ${leaked.join(', ')}`);
71
+ }
72
+ }
73
+ return { manifest, files, errors };
74
+ }
75
+ //# sourceMappingURL=bundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle.js","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":";;AAwCA,kCA6CC;AArFD,qCAA8D;AAC9D,yCAA2C;AAC3C,mEAUwC;AAUxC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,cAAc,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;AAE3E,SAAS,IAAI,CAAC,GAAW,EAAE,IAAY,EAAE,GAAiB;IACxD,KAAK,MAAM,IAAI,IAAI,IAAA,qBAAW,EAAC,GAAG,CAAC,EAAE,CAAC;QACpC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QACrC,MAAM,YAAY,GAAG,IAAA,gBAAI,EAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,IAAA,kBAAQ,EAAC,YAAY,CAAC,CAAC;QACpC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACvB,IAAI,CAAC,YAAY,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC;QAChC,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAA,oBAAQ,EAAC,IAAI,EAAE,YAAY,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;QACxG,CAAC;IACH,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,2FAA2F;AAC3F,+BAA+B;AAC/B,SAAgB,WAAW,CAAC,IAAY;IACtC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,IAAI,CAAC;QACH,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,0BAA0B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;IACvH,CAAC;IAED,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,kCAAiB,CAAC,CAAC;IACrE,IAAI,QAAQ,GAAmC,IAAI,CAAC;IACpD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,CAAC,IAAI,CAAC,MAAM,kCAAiB,2BAA2B,CAAC,CAAC;IAClE,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,IAAA,8BAAa,EAAC,IAAA,sBAAY,EAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;QAC9E,IAAI,MAAM,CAAC,KAAK;YAAE,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;;YACxC,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACrC,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,GAAG,iCAAgB;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,qBAAqB,iCAAgB,EAAE,CAAC,CAAC;IACzG,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC;QACnB,IAAI,CAAC,oCAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;aAC/E,IAAI,IAAA,kCAAiB,EAAC,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI;YAAE,MAAM,CAAC,IAAI,CAAC,0BAA0B,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACnG,IAAI,IAAI,CAAC,IAAI,GAAG,sCAAqB;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,8BAA8B,sCAAqB,EAAE,CAAC,CAAC;QACtI,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,WAAW,CAAC,CAAC;IAC5D,CAAC;IACD,IAAI,KAAK,GAAG,uCAAsB;QAAE,MAAM,CAAC,IAAI,CAAC,aAAa,KAAK,qBAAqB,uCAAsB,EAAE,CAAC,CAAC;IAEjH,IAAI,QAAQ,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9D,MAAM,CAAC,IAAI,CAAC,UAAU,QAAQ,CAAC,KAAK,gCAAgC,CAAC,CAAC;IACxE,CAAC;IAED,sFAAsF;IACtF,sFAAsF;IACtF,IAAI,QAAQ,EAAE,YAAY,KAAK,KAAK,EAAE,CAAC;QACrC,MAAM,MAAM,GAAG,IAAA,qCAAoB,EAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9D,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,yEAAyE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5G,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,11 @@
1
+ export type CliCredentials = {
2
+ apiUrl: string;
3
+ token: string;
4
+ expiresAt: string;
5
+ username: string;
6
+ };
7
+ export declare const DEFAULT_API_URL: string;
8
+ export declare function saveCredentials(creds: CliCredentials): void;
9
+ export declare function loadCredentials(): CliCredentials | null;
10
+ export declare function clearCredentials(): void;
11
+ export declare function jwtExpiresAtIso(token: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_API_URL = void 0;
4
+ exports.saveCredentials = saveCredentials;
5
+ exports.loadCredentials = loadCredentials;
6
+ exports.clearCredentials = clearCredentials;
7
+ exports.jwtExpiresAtIso = jwtExpiresAtIso;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = require("node:path");
11
+ const CONFIG_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.helix');
12
+ const CREDENTIALS_FILE = (0, node_path_1.join)(CONFIG_DIR, 'credentials.json');
13
+ exports.DEFAULT_API_URL = process.env.HELIX_API_URL ?? 'http://localhost:5000';
14
+ function saveCredentials(creds) {
15
+ (0, node_fs_1.mkdirSync)(CONFIG_DIR, { recursive: true });
16
+ (0, node_fs_1.writeFileSync)(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), {
17
+ mode: 0o600,
18
+ });
19
+ try {
20
+ (0, node_fs_1.chmodSync)(CREDENTIALS_FILE, 0o600);
21
+ }
22
+ catch {
23
+ // Windows: mode bits are advisory; the file lives under the user profile.
24
+ }
25
+ }
26
+ function loadCredentials() {
27
+ try {
28
+ const creds = JSON.parse((0, node_fs_1.readFileSync)(CREDENTIALS_FILE, 'utf8'));
29
+ if (creds.expiresAt && new Date(creds.expiresAt).getTime() < Date.now()) {
30
+ return null;
31
+ }
32
+ return creds;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function clearCredentials() {
39
+ try {
40
+ (0, node_fs_1.writeFileSync)(CREDENTIALS_FILE, '{}', { mode: 0o600 });
41
+ }
42
+ catch {
43
+ // nothing stored — already logged out.
44
+ }
45
+ }
46
+ // Reads the `exp` (seconds) claim from a JWT without verifying its signature —
47
+ // just to surface a friendly expiry to the user and to expire the stored
48
+ // credentials locally. Returns '' if the token is not a decodable JWT.
49
+ function jwtExpiresAtIso(token) {
50
+ try {
51
+ const payload = token.split('.')[1];
52
+ if (!payload)
53
+ return '';
54
+ const json = Buffer.from(payload, 'base64url').toString('utf8');
55
+ const exp = JSON.parse(json).exp;
56
+ return typeof exp === 'number' ? new Date(exp * 1000).toISOString() : '';
57
+ }
58
+ catch {
59
+ return '';
60
+ }
61
+ }
62
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":";;;AAkBA,0CAUC;AAED,0CAYC;AAED,4CAMC;AAKD,0CAUC;AAjED,qCAA4E;AAC5E,qCAAkC;AAClC,yCAAiC;AAUjC,MAAM,UAAU,GAAG,IAAA,gBAAI,EAAC,IAAA,iBAAO,GAAE,EAAE,QAAQ,CAAC,CAAC;AAC7C,MAAM,gBAAgB,GAAG,IAAA,gBAAI,EAAC,UAAU,EAAE,kBAAkB,CAAC,CAAC;AAEjD,QAAA,eAAe,GAC1B,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,uBAAuB,CAAC;AAEvD,SAAgB,eAAe,CAAC,KAAqB;IACnD,IAAA,mBAAS,EAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,IAAA,uBAAa,EAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE;QAC9D,IAAI,EAAE,KAAK;KACZ,CAAC,CAAC;IACH,IAAI,CAAC;QACH,IAAA,mBAAS,EAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;IAC5E,CAAC;AACH,CAAC;AAED,SAAgB,eAAe;IAC7B,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CACtB,IAAA,sBAAY,EAAC,gBAAgB,EAAE,MAAM,CAAC,CACrB,CAAC;QACpB,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YACxE,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAgB,gBAAgB;IAC9B,IAAI,CAAC;QACH,IAAA,uBAAa,EAAC,gBAAgB,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,uCAAuC;IACzC,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,yEAAyE;AACzE,uEAAuE;AACvE,SAAgB,eAAe,CAAC,KAAa;IAC3C,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAChE,MAAM,GAAG,GAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAsB,CAAC,GAAG,CAAC;QACvD,OAAO,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};