@hyperfixation/cli 0.1.0 → 0.1.2

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.
Files changed (75) hide show
  1. package/dist/app.d.ts +15 -2
  2. package/dist/app.js +4 -2
  3. package/dist/backup-source.d.ts +47 -0
  4. package/dist/backup-source.js +107 -0
  5. package/dist/bootstrap.d.ts +2 -0
  6. package/dist/bootstrap.js +1 -1
  7. package/dist/checklist.d.ts +25 -0
  8. package/dist/checklist.js +32 -0
  9. package/dist/cli.d.ts +2 -2
  10. package/dist/cli.js +95 -2
  11. package/dist/cloud-steps/backup.d.ts +17 -0
  12. package/dist/cloud-steps/backup.js +40 -0
  13. package/dist/cloud-steps/context.d.ts +120 -0
  14. package/dist/cloud-steps/context.js +88 -0
  15. package/dist/cloud-steps/coolify.d.ts +84 -0
  16. package/dist/cloud-steps/coolify.js +316 -0
  17. package/dist/cloud-steps/database.d.ts +12 -0
  18. package/dist/cloud-steps/database.js +25 -0
  19. package/dist/cloud-steps/deploy.d.ts +18 -0
  20. package/dist/cloud-steps/deploy.js +110 -0
  21. package/dist/cloud-steps/dns.d.ts +11 -0
  22. package/dist/cloud-steps/dns.js +53 -0
  23. package/dist/cloud-steps/index.d.ts +21 -0
  24. package/dist/cloud-steps/index.js +30 -0
  25. package/dist/cloud-steps/install.d.ts +12 -0
  26. package/dist/cloud-steps/install.js +53 -0
  27. package/dist/cloud-steps/langfuse.d.ts +17 -0
  28. package/dist/cloud-steps/langfuse.js +71 -0
  29. package/dist/cloud-steps/repo.d.ts +20 -0
  30. package/dist/cloud-steps/repo.js +198 -0
  31. package/dist/cloud-steps/sentry.d.ts +13 -0
  32. package/dist/cloud-steps/sentry.js +55 -0
  33. package/dist/cloud-steps/template.d.ts +22 -0
  34. package/dist/cloud-steps/template.js +68 -0
  35. package/dist/config.d.ts +65 -0
  36. package/dist/config.js +192 -0
  37. package/dist/database.d.ts +95 -0
  38. package/dist/database.js +226 -0
  39. package/dist/doctor.d.ts +72 -0
  40. package/dist/doctor.js +368 -0
  41. package/dist/index.d.ts +6 -1
  42. package/dist/index.js +5 -0
  43. package/dist/migrate.d.ts +11 -0
  44. package/dist/migrate.js +26 -2
  45. package/dist/new-cloud.d.ts +135 -0
  46. package/dist/new-cloud.js +219 -0
  47. package/dist/new.d.ts +2 -0
  48. package/dist/new.js +2 -1
  49. package/dist/providers/cloudflare.d.ts +49 -0
  50. package/dist/providers/cloudflare.js +27 -0
  51. package/dist/providers/coolify.d.ts +148 -0
  52. package/dist/providers/coolify.js +87 -0
  53. package/dist/providers/github.d.ts +117 -0
  54. package/dist/providers/github.js +98 -0
  55. package/dist/providers/http.d.ts +41 -0
  56. package/dist/providers/http.js +56 -0
  57. package/dist/providers/langfuse.d.ts +41 -0
  58. package/dist/providers/langfuse.js +29 -0
  59. package/dist/providers/sentry.d.ts +31 -0
  60. package/dist/providers/sentry.js +27 -0
  61. package/dist/provision-database.d.ts +42 -0
  62. package/dist/provision-database.js +107 -0
  63. package/dist/restore-check.d.ts +91 -0
  64. package/dist/restore-check.js +262 -0
  65. package/dist/runner.d.ts +72 -0
  66. package/dist/runner.js +221 -0
  67. package/dist/secret-file.d.ts +30 -0
  68. package/dist/secret-file.js +69 -0
  69. package/dist/state.d.ts +124 -0
  70. package/dist/state.js +217 -0
  71. package/dist/status-token.d.ts +2 -0
  72. package/dist/status-token.js +1 -1
  73. package/dist/template-source.d.ts +23 -0
  74. package/dist/template-source.js +23 -0
  75. package/package.json +10 -7
@@ -0,0 +1,87 @@
1
+ import { createTransport, segment } from "./http.js";
2
+ export class CoolifyClient {
3
+ request;
4
+ constructor(options) {
5
+ this.request = createTransport({
6
+ provider: "coolify",
7
+ baseUrl: `${options.url.replace(/\/+$/, "")}/api/v1`,
8
+ headers: { authorization: `Bearer ${options.token}` },
9
+ fetch: options.fetch,
10
+ });
11
+ }
12
+ async createProject(body) {
13
+ return await this.request({ method: "POST", path: "/projects", body });
14
+ }
15
+ /** Every project on the instance; `hf new` finds its own by name rather than creating a second. */
16
+ async listProjects() {
17
+ return await this.request({ method: "GET", path: "/projects" });
18
+ }
19
+ async getProject(uuid) {
20
+ return await this.request({ method: "GET", path: `/projects/${segment(uuid)}` });
21
+ }
22
+ async listEnvironments(projectUuid) {
23
+ return await this.request({
24
+ method: "GET",
25
+ path: `/projects/${segment(projectUuid)}/environments`,
26
+ });
27
+ }
28
+ async createPrivateGithubAppApplication(body) {
29
+ return await this.request({ method: "POST", path: "/applications/private-github-app", body });
30
+ }
31
+ /**
32
+ * Every application, so a rerun can find the one it created last time by name.
33
+ *
34
+ * The state cache is the first place to look for the uuid; this is what answers the case where
35
+ * the application exists but the state file does not, which is a cold run against a live app.
36
+ */
37
+ async listApplications(options = {}) {
38
+ return await this.request({ method: "GET", path: "/applications", query: { tag: options.tag } });
39
+ }
40
+ async updateEnvsBulk(appUuid, data) {
41
+ return await this.request({
42
+ method: "PATCH",
43
+ path: `/applications/${segment(appUuid)}/envs/bulk`,
44
+ body: { data },
45
+ });
46
+ }
47
+ async deploy(uuid, options = {}) {
48
+ return await this.request({
49
+ method: "POST",
50
+ path: "/deploy",
51
+ query: { uuid, force: options.force },
52
+ });
53
+ }
54
+ async getDeployment(deploymentUuid) {
55
+ return await this.request({
56
+ method: "GET",
57
+ path: `/deployments/${segment(deploymentUuid)}`,
58
+ });
59
+ }
60
+ async createDatabaseBackup(databaseUuid, body) {
61
+ return await this.request({
62
+ method: "POST",
63
+ path: `/databases/${segment(databaseUuid)}/backups`,
64
+ body,
65
+ });
66
+ }
67
+ /**
68
+ * The database's scheduled backups.
69
+ *
70
+ * `unknown`, not a model: upstream documents this response as a string whose example reads
71
+ * "Content is very complex. Will be implemented later.", so there is nothing to type against.
72
+ * A caller that needs a field has to narrow it against the box itself.
73
+ */
74
+ async listDatabaseBackups(databaseUuid) {
75
+ return await this.request({
76
+ method: "GET",
77
+ path: `/databases/${segment(databaseUuid)}/backups`,
78
+ });
79
+ }
80
+ /** The database as Coolify holds it — undocumented in shape, same as the backups list. */
81
+ async getDatabase(uuid) {
82
+ return await this.request({ method: "GET", path: `/databases/${segment(uuid)}` });
83
+ }
84
+ async updateDatabase(uuid, body) {
85
+ return await this.request({ method: "PATCH", path: `/databases/${segment(uuid)}`, body });
86
+ }
87
+ }
@@ -0,0 +1,117 @@
1
+ import { type FetchLike } from "./http.js";
2
+ export declare const GITHUB_API_URL = "https://api.github.com";
3
+ export interface GithubRepository {
4
+ full_name: string;
5
+ clone_url: string;
6
+ default_branch: string;
7
+ private: boolean;
8
+ }
9
+ export interface GithubReference {
10
+ ref: string;
11
+ object: {
12
+ sha: string;
13
+ type: string;
14
+ };
15
+ }
16
+ export interface GithubPullRequest {
17
+ number: number;
18
+ title: string;
19
+ html_url: string;
20
+ head: {
21
+ ref: string;
22
+ sha: string;
23
+ };
24
+ }
25
+ export interface GithubCombinedStatus {
26
+ /** `success` | `pending` | `failure`. */
27
+ state: string;
28
+ total_count: number;
29
+ }
30
+ /** `GET /users/{username}`, for the one question `hf new` asks of it: account or organization. */
31
+ export interface GithubUser {
32
+ login: string;
33
+ /** `User` | `Organization`; which of the two repository-creation endpoints applies. */
34
+ type: string;
35
+ }
36
+ export interface GithubInstallation {
37
+ id: number;
38
+ app_id: number;
39
+ /** The slug `HF_GITHUB_APP_SLUGS` names — Coolify's app, and the bump bot's. */
40
+ app_slug: string;
41
+ }
42
+ export interface GithubInstallations {
43
+ total_count: number;
44
+ installations: GithubInstallation[];
45
+ }
46
+ export interface GithubInstallationRepositories {
47
+ total_count: number;
48
+ repository_selection?: string;
49
+ repositories: GithubRepository[];
50
+ }
51
+ export interface GithubRepositoryRequest {
52
+ name: string;
53
+ description?: string;
54
+ private?: boolean;
55
+ auto_init?: boolean;
56
+ }
57
+ export interface GithubClientOptions {
58
+ /** `HF_GITHUB_TOKEN`. */
59
+ token: string;
60
+ url?: string;
61
+ fetch?: FetchLike;
62
+ }
63
+ export declare class GithubClient {
64
+ private readonly request;
65
+ constructor(options: GithubClientOptions);
66
+ /**
67
+ * Who `HF_GITHUB_OWNER` is: `type` decides between `/user/repos` and `/orgs/{org}/repos`.
68
+ *
69
+ * Unauthenticated-shaped data on purpose — this endpoint answers for any account, so it is the
70
+ * cheapest way to settle the question without assuming the token owns the name.
71
+ */
72
+ getUser(username: string): Promise<GithubUser>;
73
+ /**
74
+ * The repository, when there may already be one.
75
+ *
76
+ * A 404 from here is "no such repository **for this token**": the same status covers absent and
77
+ * invisible, so a caller that means to create one must treat it as "create and let the create
78
+ * fail" rather than as proof the name is free.
79
+ */
80
+ getRepository(owner: string, repo: string): Promise<GithubRepository>;
81
+ /** The repository for an app whose `HF_GITHUB_OWNER` is the token's own account. */
82
+ createUserRepository(body: GithubRepositoryRequest): Promise<GithubRepository>;
83
+ /** The same, when `HF_GITHUB_OWNER` is an organization instead. */
84
+ createOrgRepository(org: string, body: GithubRepositoryRequest): Promise<GithubRepository>;
85
+ /**
86
+ * `hf doctor`'s idea of what the repository says is deployed. `ref` is `heads/main`, and is
87
+ * not percent-encoded: GitHub spells this parameter with the slash as a path separator.
88
+ */
89
+ getReference(owner: string, repo: string, ref: string): Promise<GithubReference>;
90
+ /**
91
+ * Open pull requests, unfiltered.
92
+ *
93
+ * `hf doctor` wants the `core-bump/` ones, but GitHub's `head` filter takes a whole
94
+ * `user:branch`, not a prefix, so the prefix match belongs to the caller.
95
+ */
96
+ listPullRequests(owner: string, repo: string, options?: {
97
+ state?: "open" | "closed" | "all";
98
+ per_page?: number;
99
+ }): Promise<GithubPullRequest[]>;
100
+ /**
101
+ * The GitHub Apps installed for the token's user, with their slugs.
102
+ *
103
+ * `hf new` asserts both `HF_GITHUB_APP_SLUGS` entries are installed on the new repository —
104
+ * Coolify cannot deploy from a repository its app cannot see, and that failure otherwise
105
+ * surfaces as a deploy that clones nothing.
106
+ */
107
+ listInstallations(options?: {
108
+ per_page?: number;
109
+ page?: number;
110
+ }): Promise<GithubInstallations>;
111
+ /** Which repositories one installation actually reaches; the other half of that assertion. */
112
+ listInstallationRepositories(installationId: number, options?: {
113
+ per_page?: number;
114
+ page?: number;
115
+ }): Promise<GithubInstallationRepositories>;
116
+ getCombinedStatus(owner: string, repo: string, ref: string): Promise<GithubCombinedStatus>;
117
+ }
@@ -0,0 +1,98 @@
1
+ import { createTransport, segment } from "./http.js";
2
+ export const GITHUB_API_URL = "https://api.github.com";
3
+ export class GithubClient {
4
+ request;
5
+ constructor(options) {
6
+ this.request = createTransport({
7
+ provider: "github",
8
+ baseUrl: options.url ?? GITHUB_API_URL,
9
+ headers: {
10
+ authorization: `Bearer ${options.token}`,
11
+ accept: "application/vnd.github+json",
12
+ "x-github-api-version": "2022-11-28",
13
+ },
14
+ fetch: options.fetch,
15
+ });
16
+ }
17
+ /**
18
+ * Who `HF_GITHUB_OWNER` is: `type` decides between `/user/repos` and `/orgs/{org}/repos`.
19
+ *
20
+ * Unauthenticated-shaped data on purpose — this endpoint answers for any account, so it is the
21
+ * cheapest way to settle the question without assuming the token owns the name.
22
+ */
23
+ async getUser(username) {
24
+ return await this.request({ method: "GET", path: `/users/${segment(username)}` });
25
+ }
26
+ /**
27
+ * The repository, when there may already be one.
28
+ *
29
+ * A 404 from here is "no such repository **for this token**": the same status covers absent and
30
+ * invisible, so a caller that means to create one must treat it as "create and let the create
31
+ * fail" rather than as proof the name is free.
32
+ */
33
+ async getRepository(owner, repo) {
34
+ return await this.request({
35
+ method: "GET",
36
+ path: `/repos/${segment(owner)}/${segment(repo)}`,
37
+ });
38
+ }
39
+ /** The repository for an app whose `HF_GITHUB_OWNER` is the token's own account. */
40
+ async createUserRepository(body) {
41
+ return await this.request({ method: "POST", path: "/user/repos", body });
42
+ }
43
+ /** The same, when `HF_GITHUB_OWNER` is an organization instead. */
44
+ async createOrgRepository(org, body) {
45
+ return await this.request({ method: "POST", path: `/orgs/${segment(org)}/repos`, body });
46
+ }
47
+ /**
48
+ * `hf doctor`'s idea of what the repository says is deployed. `ref` is `heads/main`, and is
49
+ * not percent-encoded: GitHub spells this parameter with the slash as a path separator.
50
+ */
51
+ async getReference(owner, repo, ref) {
52
+ return await this.request({
53
+ method: "GET",
54
+ path: `/repos/${segment(owner)}/${segment(repo)}/git/ref/${ref}`,
55
+ });
56
+ }
57
+ /**
58
+ * Open pull requests, unfiltered.
59
+ *
60
+ * `hf doctor` wants the `core-bump/` ones, but GitHub's `head` filter takes a whole
61
+ * `user:branch`, not a prefix, so the prefix match belongs to the caller.
62
+ */
63
+ async listPullRequests(owner, repo, options = {}) {
64
+ return await this.request({
65
+ method: "GET",
66
+ path: `/repos/${segment(owner)}/${segment(repo)}/pulls`,
67
+ query: { state: options.state, per_page: options.per_page },
68
+ });
69
+ }
70
+ /**
71
+ * The GitHub Apps installed for the token's user, with their slugs.
72
+ *
73
+ * `hf new` asserts both `HF_GITHUB_APP_SLUGS` entries are installed on the new repository —
74
+ * Coolify cannot deploy from a repository its app cannot see, and that failure otherwise
75
+ * surfaces as a deploy that clones nothing.
76
+ */
77
+ async listInstallations(options = {}) {
78
+ return await this.request({
79
+ method: "GET",
80
+ path: "/user/installations",
81
+ query: { per_page: options.per_page, page: options.page },
82
+ });
83
+ }
84
+ /** Which repositories one installation actually reaches; the other half of that assertion. */
85
+ async listInstallationRepositories(installationId, options = {}) {
86
+ return await this.request({
87
+ method: "GET",
88
+ path: `/user/installations/${segment(String(installationId))}/repositories`,
89
+ query: { per_page: options.per_page, page: options.page },
90
+ });
91
+ }
92
+ async getCombinedStatus(owner, repo, ref) {
93
+ return await this.request({
94
+ method: "GET",
95
+ path: `/repos/${segment(owner)}/${segment(repo)}/commits/${segment(ref)}/status`,
96
+ });
97
+ }
98
+ }
@@ -0,0 +1,41 @@
1
+ /** The one function every client talks to the network through; tests inject their own. */
2
+ export type FetchLike = typeof globalThis.fetch;
3
+ export type HttpMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
4
+ export interface ProviderRequest {
5
+ method: HttpMethod;
6
+ /** Path under the client's base URL, already interpolated. */
7
+ path: string;
8
+ query?: Record<string, string | number | boolean | undefined>;
9
+ body?: unknown;
10
+ }
11
+ /**
12
+ * A provider answered with a status outside 2xx.
13
+ *
14
+ * The response body is on `body` and deliberately **not** in `message`: a Coolify 422 echoes
15
+ * the fields it rejected, and those fields are the app's whole environment — its database URL,
16
+ * its status tokens, its Langfuse secret key. `hf` prints `error.message`, so a body that
17
+ * quotes a secret would land in a terminal and a scrollback. Nor is the query string included,
18
+ * for the same reason.
19
+ */
20
+ export declare class ProviderError extends Error {
21
+ readonly provider: string;
22
+ readonly status: number;
23
+ readonly method: HttpMethod;
24
+ readonly path: string;
25
+ readonly body: string;
26
+ constructor(provider: string, request: ProviderRequest, status: number, body: string);
27
+ }
28
+ export interface TransportOptions {
29
+ /** Names the provider in errors. */
30
+ provider: string;
31
+ /** Everything before `path`, including any API prefix (`https://coolify.example/api/v1`). */
32
+ baseUrl: string;
33
+ /** Sent on every request — the authorization header, and whatever else the API insists on. */
34
+ headers: Record<string, string>;
35
+ fetch?: FetchLike;
36
+ }
37
+ export type Transport = <Result>(request: ProviderRequest) => Promise<Result>;
38
+ /** Builds the `request` function the clients in this directory are written against. */
39
+ export declare function createTransport(options: TransportOptions): Transport;
40
+ /** Percent-encodes one path segment, so an app name can never escape into the path. */
41
+ export declare function segment(value: string): string;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A provider answered with a status outside 2xx.
3
+ *
4
+ * The response body is on `body` and deliberately **not** in `message`: a Coolify 422 echoes
5
+ * the fields it rejected, and those fields are the app's whole environment — its database URL,
6
+ * its status tokens, its Langfuse secret key. `hf` prints `error.message`, so a body that
7
+ * quotes a secret would land in a terminal and a scrollback. Nor is the query string included,
8
+ * for the same reason.
9
+ */
10
+ export class ProviderError extends Error {
11
+ provider;
12
+ status;
13
+ method;
14
+ path;
15
+ body;
16
+ constructor(provider, request, status, body) {
17
+ super(`${provider} ${request.method} ${request.path} failed: HTTP ${status}`);
18
+ this.name = "ProviderError";
19
+ this.provider = provider;
20
+ this.status = status;
21
+ this.method = request.method;
22
+ this.path = request.path;
23
+ this.body = body;
24
+ }
25
+ }
26
+ /** Builds the `request` function the clients in this directory are written against. */
27
+ export function createTransport(options) {
28
+ // Looked up per request, not captured: a test's mock server replaces `globalThis.fetch` after
29
+ // the clients have been constructed.
30
+ const doFetch = (input, init) => (options.fetch ?? globalThis.fetch)(input, init);
31
+ const base = options.baseUrl.replace(/\/+$/, "");
32
+ return async (request) => {
33
+ const url = new URL(base + request.path);
34
+ for (const [name, value] of Object.entries(request.query ?? {})) {
35
+ if (value !== undefined)
36
+ url.searchParams.set(name, String(value));
37
+ }
38
+ const headers = { accept: "application/json", ...options.headers };
39
+ if (request.body !== undefined)
40
+ headers["content-type"] = "application/json";
41
+ const response = await doFetch(url, {
42
+ method: request.method,
43
+ headers,
44
+ body: request.body === undefined ? undefined : JSON.stringify(request.body),
45
+ });
46
+ const text = await response.text();
47
+ if (!response.ok) {
48
+ throw new ProviderError(options.provider, request, response.status, text);
49
+ }
50
+ return (text === "" ? undefined : JSON.parse(text));
51
+ };
52
+ }
53
+ /** Percent-encodes one path segment, so an app name can never escape into the path. */
54
+ export function segment(value) {
55
+ return encodeURIComponent(value);
56
+ }
@@ -0,0 +1,41 @@
1
+ import { type FetchLike } from "./http.js";
2
+ export interface LangfuseProject {
3
+ id: string;
4
+ name: string;
5
+ }
6
+ export interface LangfuseProjects {
7
+ data: LangfuseProject[];
8
+ }
9
+ export interface LangfuseApiKey {
10
+ id: string;
11
+ publicKey: string;
12
+ /** Returned once, at creation; Langfuse never shows it again. */
13
+ secretKey: string;
14
+ note?: string;
15
+ }
16
+ export interface LangfuseClientOptions {
17
+ /** `HF_LANGFUSE_URL` — the instance's origin. */
18
+ url: string;
19
+ /**
20
+ * `HF_LANGFUSE_ORG_KEY`, spelled `<publicKey>:<secretKey>`: Langfuse authenticates with HTTP
21
+ * Basic, and an organization-scoped key is a pair, so the pair is one config value rather
22
+ * than two that can be set out of step with each other.
23
+ */
24
+ orgKey: string;
25
+ fetch?: FetchLike;
26
+ }
27
+ export declare class LangfuseClient {
28
+ private readonly request;
29
+ constructor(options: LangfuseClientOptions);
30
+ /** The org key's projects; a rerun finds the app's own by name instead of creating a second. */
31
+ listProjects(): Promise<LangfuseProjects>;
32
+ /** `retention` is required by the API: 0 keeps data indefinitely. */
33
+ createProject(body: {
34
+ name: string;
35
+ retention: number;
36
+ metadata?: Record<string, unknown>;
37
+ }): Promise<LangfuseProject>;
38
+ createApiKey(projectId: string, body?: {
39
+ note?: string;
40
+ }): Promise<LangfuseApiKey>;
41
+ }
@@ -0,0 +1,29 @@
1
+ import { createTransport, segment } from "./http.js";
2
+ export class LangfuseClient {
3
+ request;
4
+ constructor(options) {
5
+ this.request = createTransport({
6
+ provider: "langfuse",
7
+ baseUrl: options.url.replace(/\/+$/, ""),
8
+ headers: {
9
+ authorization: `Basic ${Buffer.from(options.orgKey, "utf8").toString("base64")}`,
10
+ },
11
+ fetch: options.fetch,
12
+ });
13
+ }
14
+ /** The org key's projects; a rerun finds the app's own by name instead of creating a second. */
15
+ async listProjects() {
16
+ return await this.request({ method: "GET", path: "/api/public/projects" });
17
+ }
18
+ /** `retention` is required by the API: 0 keeps data indefinitely. */
19
+ async createProject(body) {
20
+ return await this.request({ method: "POST", path: "/api/public/projects", body });
21
+ }
22
+ async createApiKey(projectId, body = {}) {
23
+ return await this.request({
24
+ method: "POST",
25
+ path: `/api/public/projects/${segment(projectId)}/apiKeys`,
26
+ body,
27
+ });
28
+ }
29
+ }
@@ -0,0 +1,31 @@
1
+ import { type FetchLike } from "./http.js";
2
+ export declare const SENTRY_URL = "https://sentry.io";
3
+ export interface SentryProject {
4
+ id: string;
5
+ slug: string;
6
+ name: string;
7
+ }
8
+ export interface SentryProjectKey {
9
+ id: string;
10
+ name: string;
11
+ dsn: {
12
+ public: string;
13
+ };
14
+ }
15
+ export interface SentryClientOptions {
16
+ /** `HF_SENTRY_TOKEN`. */
17
+ token: string;
18
+ url?: string;
19
+ fetch?: FetchLike;
20
+ }
21
+ export declare class SentryClient {
22
+ private readonly request;
23
+ constructor(options: SentryClientOptions);
24
+ createProject(org: string, body: {
25
+ name: string;
26
+ slug?: string;
27
+ platform?: string;
28
+ }): Promise<SentryProject>;
29
+ /** The DSN `hf new` writes into `SENTRY_DSN` is `keys[0].dsn.public`. */
30
+ listProjectKeys(org: string, project: string): Promise<SentryProjectKey[]>;
31
+ }
@@ -0,0 +1,27 @@
1
+ import { createTransport, segment } from "./http.js";
2
+ export const SENTRY_URL = "https://sentry.io";
3
+ export class SentryClient {
4
+ request;
5
+ constructor(options) {
6
+ this.request = createTransport({
7
+ provider: "sentry",
8
+ baseUrl: options.url ?? SENTRY_URL,
9
+ headers: { authorization: `Bearer ${options.token}` },
10
+ fetch: options.fetch,
11
+ });
12
+ }
13
+ async createProject(org, body) {
14
+ return await this.request({
15
+ method: "POST",
16
+ path: `/api/0/organizations/${segment(org)}/projects/`,
17
+ body,
18
+ });
19
+ }
20
+ /** The DSN `hf new` writes into `SENTRY_DSN` is `keys[0].dsn.public`. */
21
+ async listProjectKeys(org, project) {
22
+ return await this.request({
23
+ method: "GET",
24
+ path: `/api/0/projects/${segment(org)}/${segment(project)}/keys/`,
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,42 @@
1
+ import { type RoleNames } from "@hyperfixation/db/migrator";
2
+ import { type Database } from "./database.js";
3
+ import type { AppStateStore } from "./state.js";
4
+ /** Created in the app's database before its first migration; both are `hf_*` table columns. */
5
+ export declare const REQUIRED_EXTENSIONS: readonly ["vector", "pg_trgm"];
6
+ export declare class ProvisionDatabaseError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ export interface ProvisionDatabaseOptions {
10
+ /** The name `hf new` was given; `hf_<app>` and the three roles derive from it. */
11
+ app: string;
12
+ state: AppStateStore;
13
+ /** Create the `_ro` role Metabase reads through. Default true. */
14
+ readonlyRole?: boolean;
15
+ }
16
+ export interface ProvisionDatabaseResult {
17
+ databaseName: string;
18
+ roles: RoleNames;
19
+ createdDatabase: boolean;
20
+ /** Nothing was issued: the `database` step was already recorded. */
21
+ alreadyDone: boolean;
22
+ /**
23
+ * A role that already existed was given a new password — a cold run against a live app.
24
+ *
25
+ * The deployed containers still hold the old one, so E3 has to order this
26
+ * rotate → Coolify env → redeploy; a caller that ignores this locks the app out of its own
27
+ * database until the next deploy.
28
+ */
29
+ rotated: boolean;
30
+ }
31
+ /**
32
+ * The app's database, its extensions and its three roles, resumable and safe to rerun.
33
+ *
34
+ * Passwords live only in the state cache, and the order here is what keeps that honest: every
35
+ * password is written to the file **after** the cluster has accepted it, never before. A crash
36
+ * in between therefore leaves a state file that lags the database rather than one that leads
37
+ * it, and the next run — which still sees the step unrecorded, and still has no password to
38
+ * reuse — generates a fresh one and `ALTER`s again. Converging costs one more rotation; the
39
+ * other order would leave a file whose passwords nothing can log in with, and those files are
40
+ * the only copy there is.
41
+ */
42
+ export declare function provisionDatabase(target: Database | string, options: ProvisionDatabaseOptions): Promise<ProvisionDatabaseResult>;