@hyperfixation/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.
@@ -0,0 +1,15 @@
1
+ export class MissingEnv extends Error {
2
+ names;
3
+ constructor(names, dir) {
4
+ super(`${names.join(", ")} unset: set ${names.length === 1 ? "it" : "them"} in ${dir}/.env`);
5
+ this.name = "MissingEnv";
6
+ this.names = names;
7
+ }
8
+ }
9
+ /** Reads one var out of `.env`-under-`process.env`, or names the file the user has to edit. */
10
+ export function requireEnv(app, name) {
11
+ const value = app.env[name];
12
+ if (value === undefined || value === "")
13
+ throw new MissingEnv([name], app.dir);
14
+ return value;
15
+ }
@@ -0,0 +1,34 @@
1
+ export interface LocalRoleOptions {
2
+ databaseName: string;
3
+ applicationRole: string;
4
+ applicationPassword: string;
5
+ }
6
+ export interface LocalRoleResult {
7
+ applicationRole: string;
8
+ created: boolean;
9
+ }
10
+ /**
11
+ * Creates the application role for a **local** app, on the migrator's own connection.
12
+ *
13
+ * `@hyperfixation/db`'s `provisionRoles` is the cloud path and is not usable here: it derives
14
+ * both role names from the app name and records the application role's default privileges
15
+ * `FOR ROLE hf_<app>_migrator`. Locally the migrator is the compose superuser `postgres` — that
16
+ * is what track B's `.env.example` puts in `MIGRATOR_DATABASE_URL` — so privileges recorded for
17
+ * a role that creates nothing would leave the application role unable to read the tables the
18
+ * migrator just made. The grant here is recorded for whoever is connected, which is the role
19
+ * that will own every `hf_*` object on this machine.
20
+ *
21
+ * The connection therefore has to be able to create a role, which the compose superuser can and
22
+ * a deployed migrator role cannot — `hf migrate --skip-roles` is the cloud path, where `hf new`
23
+ * created both roles before the first deploy.
24
+ *
25
+ * Idempotent: an existing role is re-`ALTER`ed to the password `.env` declares, and the grants
26
+ * on already-existing objects are re-issued, so running it after the first `hf migrate` is a
27
+ * no-op rather than a permissions gap.
28
+ */
29
+ export declare function provisionLocalRoles(migratorConnectionString: string, options: LocalRoleOptions): Promise<LocalRoleResult>;
30
+ /** The role and password a `DATABASE_URL` carries — what the application role has to become. */
31
+ export declare function credentialsOf(connectionString: string): {
32
+ user: string;
33
+ password: string;
34
+ };
package/dist/roles.js ADDED
@@ -0,0 +1,51 @@
1
+ import { quoteIdent } from "@hyperfixation/db";
2
+ import { Client } from "pg";
3
+ /**
4
+ * Creates the application role for a **local** app, on the migrator's own connection.
5
+ *
6
+ * `@hyperfixation/db`'s `provisionRoles` is the cloud path and is not usable here: it derives
7
+ * both role names from the app name and records the application role's default privileges
8
+ * `FOR ROLE hf_<app>_migrator`. Locally the migrator is the compose superuser `postgres` — that
9
+ * is what track B's `.env.example` puts in `MIGRATOR_DATABASE_URL` — so privileges recorded for
10
+ * a role that creates nothing would leave the application role unable to read the tables the
11
+ * migrator just made. The grant here is recorded for whoever is connected, which is the role
12
+ * that will own every `hf_*` object on this machine.
13
+ *
14
+ * The connection therefore has to be able to create a role, which the compose superuser can and
15
+ * a deployed migrator role cannot — `hf migrate --skip-roles` is the cloud path, where `hf new`
16
+ * created both roles before the first deploy.
17
+ *
18
+ * Idempotent: an existing role is re-`ALTER`ed to the password `.env` declares, and the grants
19
+ * on already-existing objects are re-issued, so running it after the first `hf migrate` is a
20
+ * no-op rather than a permissions gap.
21
+ */
22
+ export async function provisionLocalRoles(migratorConnectionString, options) {
23
+ const role = quoteIdent(options.applicationRole);
24
+ const client = new Client({ connectionString: migratorConnectionString });
25
+ await client.connect();
26
+ try {
27
+ const { rows } = await client.query("SELECT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = $1) AS exists", [options.applicationRole]);
28
+ const created = rows[0]?.exists !== true;
29
+ await client.query(`${created ? "CREATE" : "ALTER"} ROLE ${role} LOGIN PASSWORD ` +
30
+ `${quoteLiteral(options.applicationPassword)} CONNECTION LIMIT 25`);
31
+ await client.query(`GRANT CONNECT ON DATABASE ${quoteIdent(options.databaseName)} TO ${role}`);
32
+ await client.query(`GRANT USAGE ON SCHEMA public TO ${role}`);
33
+ await client.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public ` +
34
+ `GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO ${role}`);
35
+ await client.query(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO ${role}`);
36
+ await client.query(`GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO ${role}`);
37
+ await client.query(`GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO ${role}`);
38
+ return { applicationRole: options.applicationRole, created };
39
+ }
40
+ finally {
41
+ await client.end();
42
+ }
43
+ }
44
+ /** The role and password a `DATABASE_URL` carries — what the application role has to become. */
45
+ export function credentialsOf(connectionString) {
46
+ const url = new URL(connectionString);
47
+ return { user: decodeURIComponent(url.username), password: decodeURIComponent(url.password) };
48
+ }
49
+ function quoteLiteral(value) {
50
+ return `'${value.replace(/'/g, "''")}'`;
51
+ }
@@ -0,0 +1,20 @@
1
+ import { type StdioOptions } from "node:child_process";
2
+ export declare class CommandFailed extends Error {
3
+ readonly exitCode: number | null;
4
+ readonly signal: NodeJS.Signals | null;
5
+ constructor(command: string, exitCode: number | null, signal: NodeJS.Signals | null);
6
+ }
7
+ export interface RunOptions {
8
+ cwd: string;
9
+ env?: NodeJS.ProcessEnv;
10
+ stdio?: StdioOptions;
11
+ }
12
+ /**
13
+ * Runs a child to completion and throws on a non-zero exit.
14
+ *
15
+ * `stdio: "inherit"` by default: every command this spawns — the migrator, `turbo gen`, `next
16
+ * dev` — is one whose output the user is meant to read, and buffering it would turn a prompt
17
+ * into a hang. `SIGINT` is deliberately not forwarded; the child shares the terminal's process
18
+ * group and gets it from the terminal at the same moment the CLI does.
19
+ */
20
+ export declare function run(command: string, args: readonly string[], options: RunOptions): Promise<void>;
package/dist/spawn.js ADDED
@@ -0,0 +1,34 @@
1
+ import { spawn } from "node:child_process";
2
+ export class CommandFailed extends Error {
3
+ exitCode;
4
+ signal;
5
+ constructor(command, exitCode, signal) {
6
+ super(`${command} exited ${signal === null ? `with code ${String(exitCode)}` : `on ${signal}`}`);
7
+ this.name = "CommandFailed";
8
+ this.exitCode = exitCode;
9
+ this.signal = signal;
10
+ }
11
+ }
12
+ /**
13
+ * Runs a child to completion and throws on a non-zero exit.
14
+ *
15
+ * `stdio: "inherit"` by default: every command this spawns — the migrator, `turbo gen`, `next
16
+ * dev` — is one whose output the user is meant to read, and buffering it would turn a prompt
17
+ * into a hang. `SIGINT` is deliberately not forwarded; the child shares the terminal's process
18
+ * group and gets it from the terminal at the same moment the CLI does.
19
+ */
20
+ export async function run(command, args, options) {
21
+ const child = spawn(command, [...args], {
22
+ cwd: options.cwd,
23
+ env: options.env ?? process.env,
24
+ stdio: options.stdio ?? "inherit",
25
+ });
26
+ const [code, signal] = await new Promise((resolve, reject) => {
27
+ child.once("error", reject);
28
+ child.once("exit", (exitCode, exitSignal) => resolve([exitCode, exitSignal]));
29
+ });
30
+ // A child killed by the terminal's own SIGINT is the user stopping `hf dev`, not a failure.
31
+ if (code === 0 || signal === "SIGINT" || signal === "SIGTERM")
32
+ return;
33
+ throw new CommandFailed([command, ...args].join(" "), code, signal);
34
+ }
@@ -0,0 +1,42 @@
1
+ import { type ResolvedApp } from "./app.js";
2
+ export type StatusTokenKind = "read" | "write";
3
+ /** One column already carries a hash and `--rotate` was not passed to authorize replacing it. */
4
+ export declare class StatusTokenAlreadySet extends Error {
5
+ readonly kind: StatusTokenKind;
6
+ constructor(kind: StatusTokenKind);
7
+ }
8
+ export interface StatusTokenAppOptions {
9
+ dir?: string;
10
+ /** Which token(s) to (re)generate. Both when omitted. */
11
+ kinds?: readonly StatusTokenKind[];
12
+ /** Overwrites a hash that is already set; otherwise a set column is refused. */
13
+ rotate?: boolean;
14
+ /**
15
+ * True when `kinds` was narrowed by an explicit `--read`/`--write`, rather than defaulted to
16
+ * both. An explicit ask for a kind that is already set is refused the same as before, since
17
+ * the operator named it on purpose; the *default* two-kind run instead skips whichever kind
18
+ * is already set and only fills the gap — otherwise "run it to provision the token nobody set
19
+ * up yet" would also require `--rotate`, and `--rotate` would take out the other, live token.
20
+ */
21
+ explicit?: boolean;
22
+ }
23
+ export interface StatusTokenAppResult {
24
+ app: ResolvedApp;
25
+ /** The plaintext of each token generated this run — the only time it is ever available. */
26
+ tokens: Partial<Record<StatusTokenKind, string>>;
27
+ }
28
+ /**
29
+ * `hf status-token` — provisions `hf_app_state.read_token_hash`/`write_token_hash`, the two
30
+ * secrets `/api/status` is authorized against (`statusTokenMatches` in `@hyperfixation/core`
31
+ * refuses every request when a hash is unset, so an app cannot serve status until this has run
32
+ * at least once).
33
+ *
34
+ * Connects as the **application** role, like `hf bootstrap`: this only ever updates the
35
+ * singleton row an app already owns, never anything a migrator-level grant is needed for.
36
+ *
37
+ * Read and write tokens are independent secrets — the write token is strictly the more
38
+ * privileged of the two (`createStatusHandler` accepts it on `GET` as well), so rotating one
39
+ * does not have to rotate the other. Each plaintext token is printed exactly once, by the
40
+ * caller; nothing here ever stores or logs it.
41
+ */
42
+ export declare function statusTokenApp(options?: StatusTokenAppOptions): Promise<StatusTokenAppResult>;
@@ -0,0 +1,90 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { hashStatusToken } from "@hyperfixation/core";
3
+ import { AppStateMissing } from "@hyperfixation/db";
4
+ import { Pool } from "pg";
5
+ import { resolveApp } from "./app.js";
6
+ import { requireEnv } from "./require-env.js";
7
+ const COLUMN = {
8
+ read: "read_token_hash",
9
+ write: "write_token_hash",
10
+ };
11
+ const SELECT_HASHES_STATEMENT = "SELECT read_token_hash, write_token_hash FROM hf_app_state WHERE id = 1 FOR UPDATE";
12
+ const AUDIT_STATEMENT = "INSERT INTO hf_audit (actor_id, action, target_type, target_id, meta) " +
13
+ "VALUES (NULL, 'app.status_token_provisioned', 'hf_app_state', '1', $1::jsonb)";
14
+ /** One column already carries a hash and `--rotate` was not passed to authorize replacing it. */
15
+ export class StatusTokenAlreadySet extends Error {
16
+ kind;
17
+ constructor(kind) {
18
+ super(`the ${kind} token is already set; pass --rotate to replace it`);
19
+ this.name = "StatusTokenAlreadySet";
20
+ this.kind = kind;
21
+ }
22
+ }
23
+ /**
24
+ * `hf status-token` — provisions `hf_app_state.read_token_hash`/`write_token_hash`, the two
25
+ * secrets `/api/status` is authorized against (`statusTokenMatches` in `@hyperfixation/core`
26
+ * refuses every request when a hash is unset, so an app cannot serve status until this has run
27
+ * at least once).
28
+ *
29
+ * Connects as the **application** role, like `hf bootstrap`: this only ever updates the
30
+ * singleton row an app already owns, never anything a migrator-level grant is needed for.
31
+ *
32
+ * Read and write tokens are independent secrets — the write token is strictly the more
33
+ * privileged of the two (`createStatusHandler` accepts it on `GET` as well), so rotating one
34
+ * does not have to rotate the other. Each plaintext token is printed exactly once, by the
35
+ * caller; nothing here ever stores or logs it.
36
+ */
37
+ export async function statusTokenApp(options = {}) {
38
+ const app = await resolveApp(options.dir);
39
+ const databaseUrl = requireEnv(app, "DATABASE_URL");
40
+ const kinds = options.kinds ?? ["read", "write"];
41
+ const pool = new Pool({ connectionString: databaseUrl, max: 1 });
42
+ try {
43
+ const client = await pool.connect();
44
+ try {
45
+ await client.query("BEGIN");
46
+ const tokens = await rotateTokens(client, kinds, options.rotate ?? false, options.explicit ?? false);
47
+ await client.query("COMMIT");
48
+ return { app, tokens };
49
+ }
50
+ catch (error) {
51
+ await client.query("ROLLBACK");
52
+ throw error;
53
+ }
54
+ finally {
55
+ client.release();
56
+ }
57
+ }
58
+ finally {
59
+ await pool.end();
60
+ }
61
+ }
62
+ async function rotateTokens(client, kinds, allowRotate, explicit) {
63
+ const { rows } = await client.query(SELECT_HASHES_STATEMENT);
64
+ const existing = rows[0];
65
+ if (existing === undefined)
66
+ throw new AppStateMissing("hf status-token");
67
+ const toGenerate = [];
68
+ for (const kind of kinds) {
69
+ const alreadySet = existing[COLUMN[kind]] !== null;
70
+ if (!alreadySet || allowRotate) {
71
+ toGenerate.push(kind);
72
+ }
73
+ else if (explicit) {
74
+ throw new StatusTokenAlreadySet(kind);
75
+ }
76
+ // Default two-kind run, already set, no --rotate: silently left alone.
77
+ }
78
+ const tokens = {};
79
+ for (const kind of toGenerate) {
80
+ const token = randomBytes(32).toString("base64url");
81
+ tokens[kind] = token;
82
+ await client.query(`UPDATE hf_app_state SET ${COLUMN[kind]} = $1 WHERE id = 1`, [
83
+ hashStatusToken(token),
84
+ ]);
85
+ }
86
+ if (toGenerate.length > 0) {
87
+ await client.query(AUDIT_STATEMENT, [JSON.stringify({ kinds: toGenerate })]);
88
+ }
89
+ return tokens;
90
+ }
@@ -0,0 +1,16 @@
1
+ /** Points `hf new` at a template checkout; what a CI job or a second checkout sets. */
2
+ export declare const TEMPLATE_DIR_ENV = "HF_TEMPLATE_DIR";
3
+ /**
4
+ * Where `hf new --local` looks when `--from` is not given: the environment, then the sibling
5
+ * checkout beside the working directory, then the sibling beside this package's own repo.
6
+ *
7
+ * The last one is what makes `hf new demo-app --local` work when `hf` is run from inside
8
+ * hyperfixation-core, which is the Phase 1 development layout the plan describes:
9
+ *
10
+ * Code/
11
+ * hyperfixation/ (this repo)
12
+ * hyperfixation-template/
13
+ */
14
+ export declare function findTemplateSource(cwd?: string): Promise<string | undefined>;
15
+ /** The same search, but a miss is the error the user has to act on rather than `undefined`. */
16
+ export declare function requireTemplateSource(cwd?: string): Promise<string>;
@@ -0,0 +1,46 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { TEMPLATE_MARKER, TemplateError } from "./new.js";
5
+ /** Points `hf new` at a template checkout; what a CI job or a second checkout sets. */
6
+ export const TEMPLATE_DIR_ENV = "HF_TEMPLATE_DIR";
7
+ /**
8
+ * Where `hf new --local` looks when `--from` is not given: the environment, then the sibling
9
+ * checkout beside the working directory, then the sibling beside this package's own repo.
10
+ *
11
+ * The last one is what makes `hf new demo-app --local` work when `hf` is run from inside
12
+ * hyperfixation-core, which is the Phase 1 development layout the plan describes:
13
+ *
14
+ * Code/
15
+ * hyperfixation/ (this repo)
16
+ * hyperfixation-template/
17
+ */
18
+ export async function findTemplateSource(cwd = process.cwd()) {
19
+ const fromEnv = process.env[TEMPLATE_DIR_ENV];
20
+ const candidates = [
21
+ ...(fromEnv === undefined || fromEnv === "" ? [] : [path.resolve(fromEnv)]),
22
+ path.resolve(cwd, "..", "hyperfixation-template"),
23
+ path.resolve(fileURLToPath(new URL("../../..", import.meta.url)), "..", "hyperfixation-template"),
24
+ ];
25
+ for (const candidate of candidates) {
26
+ if (await isTemplate(candidate))
27
+ return candidate;
28
+ }
29
+ return undefined;
30
+ }
31
+ /** The same search, but a miss is the error the user has to act on rather than `undefined`. */
32
+ export async function requireTemplateSource(cwd) {
33
+ const found = await findTemplateSource(cwd);
34
+ if (found === undefined) {
35
+ throw new TemplateError(`no hyperfixation-template checkout found: pass --from <dir> or set ${TEMPLATE_DIR_ENV}`);
36
+ }
37
+ return found;
38
+ }
39
+ async function isTemplate(dir) {
40
+ try {
41
+ return (await stat(path.join(dir, TEMPLATE_MARKER))).isFile();
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
package/dist/up.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { type ResolvedApp } from "./app.js";
2
+ import { type StatusTokenKind } from "./status-token.js";
3
+ /**
4
+ * What `hf up` seeds `hf_app_state.budget_usd` with when `HF_BOOTSTRAP_BUDGET_USD` isn't set.
5
+ * Local only: `hf bootstrap` itself still refuses to run without an explicit budget, so a
6
+ * deployed app never starts under a cap nobody chose.
7
+ */
8
+ export declare const DEV_BUDGET_USD = "10";
9
+ export interface UpOptions {
10
+ dir?: string;
11
+ }
12
+ export interface UpResult {
13
+ app: ResolvedApp;
14
+ installedDependencies: boolean;
15
+ composeStarted: boolean;
16
+ /** False when an admin already existed and `hf bootstrap` was skipped rather than rerun. */
17
+ bootstrapped: boolean;
18
+ /** True when this run seeded `DEV_BUDGET_USD` because the app's `.env` sets no budget. */
19
+ budgetDefaulted: boolean;
20
+ /** Which token kind(s) were generated this run; empty when both were already set. */
21
+ tokensProvisioned: readonly StatusTokenKind[];
22
+ }
23
+ /**
24
+ * `hf up` — the whole local QA loop in one idempotent command: install, infra, migrate,
25
+ * bootstrap, status tokens, then `hf dev` in the foreground.
26
+ *
27
+ * Every step but the last is safe to rerun. `hf bootstrap` is the one step whose own contract is
28
+ * "exactly once" (`BootstrapRefused` — an app gets one bootstrap admin, on purpose), so this
29
+ * catches that specific refusal rather than asking `bootstrapApp` to change what it means; every
30
+ * other step already no-ops on its own (`ensureCompose`'s `--wait`, `statusTokenApp`'s default
31
+ * two-kind run leaving a set column alone). `hf dev` itself is the caller's to run afterward,
32
+ * since it blocks in the foreground and this function is meant to return a result.
33
+ */
34
+ export declare function upApp(options?: UpOptions): Promise<UpResult>;
35
+ /** True when `node_modules` isn't there yet — `pnpm install` has never run for this app. */
36
+ export declare function needsInstall(dir: string): Promise<boolean>;
37
+ /** True when the app's `.env` names no budget, so `hf up` will supply `DEV_BUDGET_USD`. */
38
+ export declare function usesDefaultBudget(app: ResolvedApp): boolean;
39
+ /** Runs `hf bootstrap`; swallows `BootstrapRefused` since the app already has its one admin. */
40
+ export declare function bootstrapIfNeeded(app: ResolvedApp): Promise<boolean>;
package/dist/up.js ADDED
@@ -0,0 +1,76 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { BootstrapRefused } from "@hyperfixation/auth";
4
+ import { resolveApp } from "./app.js";
5
+ import { bootstrapApp } from "./bootstrap.js";
6
+ import { ensureCompose } from "./dev.js";
7
+ import { migrateApp } from "./migrate.js";
8
+ import { run } from "./spawn.js";
9
+ import { statusTokenApp } from "./status-token.js";
10
+ /**
11
+ * What `hf up` seeds `hf_app_state.budget_usd` with when `HF_BOOTSTRAP_BUDGET_USD` isn't set.
12
+ * Local only: `hf bootstrap` itself still refuses to run without an explicit budget, so a
13
+ * deployed app never starts under a cap nobody chose.
14
+ */
15
+ export const DEV_BUDGET_USD = "10";
16
+ const BUDGET_ENV = "HF_BOOTSTRAP_BUDGET_USD";
17
+ /**
18
+ * `hf up` — the whole local QA loop in one idempotent command: install, infra, migrate,
19
+ * bootstrap, status tokens, then `hf dev` in the foreground.
20
+ *
21
+ * Every step but the last is safe to rerun. `hf bootstrap` is the one step whose own contract is
22
+ * "exactly once" (`BootstrapRefused` — an app gets one bootstrap admin, on purpose), so this
23
+ * catches that specific refusal rather than asking `bootstrapApp` to change what it means; every
24
+ * other step already no-ops on its own (`ensureCompose`'s `--wait`, `statusTokenApp`'s default
25
+ * two-kind run leaving a set column alone). `hf dev` itself is the caller's to run afterward,
26
+ * since it blocks in the foreground and this function is meant to return a result.
27
+ */
28
+ export async function upApp(options = {}) {
29
+ const app = await resolveApp(options.dir);
30
+ const installedDependencies = await needsInstall(app.dir);
31
+ if (installedDependencies)
32
+ await run("pnpm", ["install"], { cwd: app.dir });
33
+ const composeStarted = await ensureCompose(app);
34
+ await migrateApp({ dir: app.dir });
35
+ const defaultsBudget = usesDefaultBudget(app);
36
+ const bootstrapped = await bootstrapIfNeeded(app);
37
+ const { tokens } = await statusTokenApp({ dir: app.dir });
38
+ const tokensProvisioned = Object.keys(tokens);
39
+ return {
40
+ app,
41
+ installedDependencies,
42
+ composeStarted,
43
+ bootstrapped,
44
+ budgetDefaulted: bootstrapped && defaultsBudget,
45
+ tokensProvisioned,
46
+ };
47
+ }
48
+ /** True when `node_modules` isn't there yet — `pnpm install` has never run for this app. */
49
+ export async function needsInstall(dir) {
50
+ try {
51
+ return !(await stat(path.join(dir, "node_modules"))).isDirectory();
52
+ }
53
+ catch {
54
+ return true;
55
+ }
56
+ }
57
+ /** True when the app's `.env` names no budget, so `hf up` will supply `DEV_BUDGET_USD`. */
58
+ export function usesDefaultBudget(app) {
59
+ const value = app.env[BUDGET_ENV];
60
+ return value === undefined || value === "";
61
+ }
62
+ /** Runs `hf bootstrap`; swallows `BootstrapRefused` since the app already has its one admin. */
63
+ export async function bootstrapIfNeeded(app) {
64
+ try {
65
+ await bootstrapApp({
66
+ dir: app.dir,
67
+ budgetUsd: usesDefaultBudget(app) ? DEV_BUDGET_USD : undefined,
68
+ });
69
+ return true;
70
+ }
71
+ catch (error) {
72
+ if (error instanceof BootstrapRefused)
73
+ return false;
74
+ throw error;
75
+ }
76
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@hyperfixation/cli",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "The hf binary and its Turborepo generator templates",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/grahamlutz/hyperfixation-core.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "types": "./dist/index.d.ts",
16
+ "bin": {
17
+ "hf": "./dist/bin.js"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "!dist/**/*.test.*",
29
+ "!dist/test-support/**"
30
+ ],
31
+ "dependencies": {
32
+ "@hyperfixation/auth": "0.1.0",
33
+ "@hyperfixation/core": "0.1.0",
34
+ "@hyperfixation/db": "0.1.0",
35
+ "pg": "^8.23.0"
36
+ },
37
+ "devDependencies": {
38
+ "@hyperfixation/eslint-config": "0.1.0",
39
+ "@hyperfixation/testing": "0.1.0",
40
+ "@microsoft/api-extractor": "^7.59.1",
41
+ "@types/pg": "^8.23.1",
42
+ "eslint": "^10.10.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.json",
46
+ "typecheck": "tsc -p tsconfig.json --noEmit",
47
+ "lint": "eslint src",
48
+ "api-extractor": "api-extractor run",
49
+ "api-extractor:update": "api-extractor run --local",
50
+ "test": "vitest run --passWithNoTests"
51
+ }
52
+ }