@hixumo/hive 0.0.0-stage → 0.0.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/dist/time.js ADDED
@@ -0,0 +1,42 @@
1
+ /** Dates as the sign-ins list says them: "2 Nov", "3 days ago", "in 9 hours". Local time. */
2
+ const MINUTE = 60_000;
3
+ const HOUR = 60 * MINUTE;
4
+ const DAY = 24 * HOUR;
5
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
6
+ function plural(count, unit) {
7
+ return `${count} ${unit}${count === 1 ? '' : 's'}`;
8
+ }
9
+ /** "2 Nov", with the year when it is not this one. */
10
+ export function day(iso, now) {
11
+ const date = new Date(iso);
12
+ if (Number.isNaN(date.getTime()))
13
+ return 'unknown date';
14
+ const year = date.getFullYear() === now.getFullYear() ? '' : ` ${date.getFullYear()}`;
15
+ return `${date.getDate()} ${MONTHS[date.getMonth()]}${year}`;
16
+ }
17
+ export function ago(iso, now) {
18
+ const passed = now.getTime() - new Date(iso).getTime();
19
+ if (Number.isNaN(passed))
20
+ return 'at an unknown time';
21
+ if (passed < MINUTE)
22
+ return 'just now';
23
+ if (passed < HOUR)
24
+ return `${plural(Math.floor(passed / MINUTE), 'minute')} ago`;
25
+ if (passed < DAY)
26
+ return `${plural(Math.floor(passed / HOUR), 'hour')} ago`;
27
+ if (passed < 2 * DAY)
28
+ return 'yesterday';
29
+ return `${Math.floor(passed / DAY)} days ago`;
30
+ }
31
+ export function inTime(iso, now) {
32
+ const left = new Date(iso).getTime() - now.getTime();
33
+ if (Number.isNaN(left))
34
+ return 'at an unknown time';
35
+ if (left < MINUTE)
36
+ return 'in under a minute';
37
+ if (left < HOUR)
38
+ return `in ${plural(Math.floor(left / MINUTE), 'minute')}`;
39
+ if (left < DAY)
40
+ return `in ${plural(Math.floor(left / HOUR), 'hour')}`;
41
+ return `in ${plural(Math.floor(left / DAY), 'day')}`;
42
+ }
package/dist/values.js ADDED
@@ -0,0 +1,111 @@
1
+ import { asCliError, callerOf, signedIn, tokenApi } from "./callers.js";
2
+ import { withConfirm } from "./device-flow.js";
3
+ import { assertReadable, environmentNamed, projectOf, sameName } from "./environments.js";
4
+ import { withWorkflowAccess } from "./github-actions.js";
5
+ import { findLink, isProjectId } from "./link-file.js";
6
+ import { CliError, plain } from "./output.js";
7
+ export async function readValues(ctx, options) {
8
+ try {
9
+ const caller = callerOf(ctx.env);
10
+ if (caller.kind === 'token')
11
+ return await readWithToken(ctx, caller.token, options);
12
+ if (caller.kind === 'github')
13
+ return await readAsGithubActions(ctx, caller.request, options);
14
+ return await readSignedIn(ctx, options);
15
+ }
16
+ catch (error) {
17
+ throw asCliError(error);
18
+ }
19
+ }
20
+ /** `--env`, else HIVE_ENV, and which of them said it. */
21
+ function asked(ctx, options) {
22
+ if (options.env?.trim())
23
+ return { name: options.env.trim(), by: '--env' };
24
+ const fromShell = ctx.env.HIVE_ENV?.trim();
25
+ return fromShell ? { name: fromShell, by: 'HIVE_ENV' } : undefined;
26
+ }
27
+ async function readWithToken(ctx, token, options) {
28
+ const api = tokenApi(ctx, token);
29
+ const found = linkBesideToken(ctx);
30
+ const wanted = asked(ctx, options);
31
+ // What the token is for is asked only when there is something to check it against.
32
+ let info;
33
+ if (found || wanted) {
34
+ info = await api.tokenInfo();
35
+ const project = plain(info.project.name);
36
+ const environment = plain(info.environment.name);
37
+ if (found && found.link.project !== info.project.id.toLowerCase()) {
38
+ throw new CliError(`HIVE_TOKEN is for ${project}, but ${found.path} names another project (${found.link.project}). Use this repository's token.`);
39
+ }
40
+ if (wanted && !sameName(wanted.name, info.environment.name)) {
41
+ throw new CliError(`HIVE_TOKEN reads ${environment} of ${project}, but ${wanted.by} asks for ${plain(wanted.name)}. Leave out ${wanted.by}, or use a token for ${plain(wanted.name)}.`);
42
+ }
43
+ }
44
+ const read = await api.tokenValues();
45
+ return {
46
+ values: read.values,
47
+ environment: plain(read.environment.name),
48
+ project: info && plain(info.project.name),
49
+ };
50
+ }
51
+ /**
52
+ * With a token, `.hive.json` only guards against the wrong token in a repository: one that
53
+ * cannot be read, or is not a link file, has nothing to guard with, so it is left out with one
54
+ * warning instead of stopping a deploy.
55
+ */
56
+ function linkBesideToken(ctx) {
57
+ try {
58
+ return findLink(ctx.cwd);
59
+ }
60
+ catch (error) {
61
+ if (!(error instanceof CliError))
62
+ throw error;
63
+ ctx.ui.warn(`${error.message} HIVE_TOKEN is set, so it is left out and the token decides the project.`);
64
+ return undefined;
65
+ }
66
+ }
67
+ async function readAsGithubActions(ctx, request, options) {
68
+ const target = githubTarget(ctx, options);
69
+ const read = await withWorkflowAccess(ctx, request, target, (api) => api.tokenValues());
70
+ return { values: read.values, environment: plain(read.environment.name) };
71
+ }
72
+ /**
73
+ * What a GitHub Actions run asks for, settled before GitHub is asked for anything. HIVE_PROJECT
74
+ * (set in the workflow) wins over the committed `.hive.json`, as `--env` and HIVE_ENV win over
75
+ * its environment; `.hive.json` is read only when one of the two is still missing.
76
+ */
77
+ export function githubTarget(ctx, options) {
78
+ const fromShell = ctx.env.HIVE_PROJECT?.trim();
79
+ if (fromShell && !isProjectId(fromShell)) {
80
+ throw new CliError("HIVE_PROJECT is not a project id. Use the one in the project's .hive.json (`hive link` writes it).");
81
+ }
82
+ const wanted = asked(ctx, options);
83
+ const found = fromShell && wanted ? undefined : findLink(ctx.cwd);
84
+ const project = fromShell?.toLowerCase() || found?.link.project;
85
+ if (!project) {
86
+ throw new CliError("No project to read: there is no .hive.json in this folder or above it, and HIVE_PROJECT is not set. Commit the .hive.json `hive link` writes, or set HIVE_PROJECT to the project's id.");
87
+ }
88
+ const environment = wanted?.name ?? found?.link.environment;
89
+ if (!environment) {
90
+ throw new CliError('No environment to read: pass --env (as in `--env production`), or set HIVE_ENV.');
91
+ }
92
+ return { project, environment };
93
+ }
94
+ async function readSignedIn(ctx, options) {
95
+ const { api } = await signedIn(ctx);
96
+ const found = findLink(ctx.cwd);
97
+ if (!found) {
98
+ throw new CliError('This folder is not linked to a Hive project. Run `hive link` first.');
99
+ }
100
+ const project = await projectOf(api, found.link.project, found.path);
101
+ const environment = environmentNamed(project, asked(ctx, options)?.name ?? found.link.environment);
102
+ assertReadable(project, environment);
103
+ // A protected environment needs "Confirm it is you" within 10 minutes; servers, with nobody
104
+ // to approve it, use an access token.
105
+ const name = plain(environment.name);
106
+ const { values } = await withConfirm(ctx, api, () => api.reveal(environment.id), {
107
+ why: `${name} is protected.`,
108
+ noTerminal: `${name} is protected. Use an access token on servers, or run this in a terminal.`,
109
+ });
110
+ return { values, environment: name, project: plain(project.name) };
111
+ }
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { arch, platform, versions } from 'node:process';
3
+ /** package.json sits one folder above both src/ (the tests) and dist/ (the published CLI). */
4
+ export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
5
+ /** Contract C4: which CLI, which Node, which system, for Hive's records and its sign-ins list. */
6
+ export const USER_AGENT = `hixumo-hive-cli/${VERSION} node/${versions.node} ${platform}-${arch}`;
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "@hixumo/hive",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "The Hixumo Hive CLI: run a project with its environment's values, without a .env file.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "bin": {
8
+ "hive": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=22"
17
+ },
18
+ "scripts": {
19
+ "build": "node scripts/write-build-info.mjs && tsc -p tsconfig.build.json",
20
+ "typecheck": "node scripts/write-build-info.mjs --if-missing && tsc --noEmit -p tsconfig.json",
21
+ "test": "node scripts/write-build-info.mjs --if-missing && node --test --test-concurrency=4 \"tests/**/*.test.ts\"",
22
+ "prepublishOnly": "node scripts/check-build.mjs"
23
+ },
24
+ "optionalDependencies": {
25
+ "@napi-rs/keyring": "2.1.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.20.5",
29
+ "typescript": "^5.9.3"
30
+ }
31
+ }