@ai-coders/installer 0.3.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.
Files changed (127) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +18 -0
  3. package/README.md +64 -0
  4. package/dist/callback-page.d.ts +11 -0
  5. package/dist/callback-page.js +230 -0
  6. package/dist/callback-page.js.map +1 -0
  7. package/dist/clerk-auth.d.ts +18 -0
  8. package/dist/clerk-auth.js +296 -0
  9. package/dist/clerk-auth.js.map +1 -0
  10. package/dist/cli-command.d.ts +10 -0
  11. package/dist/cli-command.js +17 -0
  12. package/dist/cli-command.js.map +1 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +84 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/i18n.d.ts +52 -0
  17. package/dist/i18n.js +163 -0
  18. package/dist/i18n.js.map +1 -0
  19. package/dist/product-manifest.d.ts +43 -0
  20. package/dist/product-manifest.js +140 -0
  21. package/dist/product-manifest.js.map +1 -0
  22. package/dist/products/second-brain/apply.d.ts +86 -0
  23. package/dist/products/second-brain/apply.js +277 -0
  24. package/dist/products/second-brain/apply.js.map +1 -0
  25. package/dist/products/second-brain/bearer.d.ts +29 -0
  26. package/dist/products/second-brain/bearer.js +78 -0
  27. package/dist/products/second-brain/bearer.js.map +1 -0
  28. package/dist/products/second-brain/bootstrap.d.ts +33 -0
  29. package/dist/products/second-brain/bootstrap.js +39 -0
  30. package/dist/products/second-brain/bootstrap.js.map +1 -0
  31. package/dist/products/second-brain/envfile.d.ts +35 -0
  32. package/dist/products/second-brain/envfile.js +136 -0
  33. package/dist/products/second-brain/envfile.js.map +1 -0
  34. package/dist/products/second-brain/github.d.ts +77 -0
  35. package/dist/products/second-brain/github.js +195 -0
  36. package/dist/products/second-brain/github.js.map +1 -0
  37. package/dist/products/second-brain/hostinger.d.ts +140 -0
  38. package/dist/products/second-brain/hostinger.js +607 -0
  39. package/dist/products/second-brain/hostinger.js.map +1 -0
  40. package/dist/products/second-brain/i18n.d.ts +887 -0
  41. package/dist/products/second-brain/i18n.js +587 -0
  42. package/dist/products/second-brain/i18n.js.map +1 -0
  43. package/dist/products/second-brain/keys.d.ts +5 -0
  44. package/dist/products/second-brain/keys.js +16 -0
  45. package/dist/products/second-brain/keys.js.map +1 -0
  46. package/dist/products/second-brain/main.d.ts +3 -0
  47. package/dist/products/second-brain/main.js +78 -0
  48. package/dist/products/second-brain/main.js.map +1 -0
  49. package/dist/products/second-brain/ports.d.ts +34 -0
  50. package/dist/products/second-brain/ports.js +77 -0
  51. package/dist/products/second-brain/ports.js.map +1 -0
  52. package/dist/products/second-brain/recovery.d.ts +50 -0
  53. package/dist/products/second-brain/recovery.js +96 -0
  54. package/dist/products/second-brain/recovery.js.map +1 -0
  55. package/dist/products/second-brain/sealed-box.d.ts +1 -0
  56. package/dist/products/second-brain/sealed-box.js +11 -0
  57. package/dist/products/second-brain/sealed-box.js.map +1 -0
  58. package/dist/products/second-brain/server.d.ts +143 -0
  59. package/dist/products/second-brain/server.js +280 -0
  60. package/dist/products/second-brain/server.js.map +1 -0
  61. package/dist/products/second-brain/service.d.ts +26 -0
  62. package/dist/products/second-brain/service.js +806 -0
  63. package/dist/products/second-brain/service.js.map +1 -0
  64. package/dist/products/second-brain/ssh.d.ts +35 -0
  65. package/dist/products/second-brain/ssh.js +140 -0
  66. package/dist/products/second-brain/ssh.js.map +1 -0
  67. package/dist/products/second-brain/state.d.ts +27 -0
  68. package/dist/products/second-brain/state.js +60 -0
  69. package/dist/products/second-brain/state.js.map +1 -0
  70. package/dist/products/second-brain/ui/assets/index-BoXpakUt.css +2 -0
  71. package/dist/products/second-brain/ui/assets/index-MgfOpW_R.js +66 -0
  72. package/dist/products/second-brain/ui/index.html +15 -0
  73. package/dist/products/second-brain/verify.d.ts +23 -0
  74. package/dist/products/second-brain/verify.js +59 -0
  75. package/dist/products/second-brain/verify.js.map +1 -0
  76. package/dist/products/severino/apply.d.ts +87 -0
  77. package/dist/products/severino/apply.js +250 -0
  78. package/dist/products/severino/apply.js.map +1 -0
  79. package/dist/products/severino/domain.d.ts +6 -0
  80. package/dist/products/severino/domain.js +20 -0
  81. package/dist/products/severino/domain.js.map +1 -0
  82. package/dist/products/severino/envfile.d.ts +20 -0
  83. package/dist/products/severino/envfile.js +89 -0
  84. package/dist/products/severino/envfile.js.map +1 -0
  85. package/dist/products/severino/github.d.ts +77 -0
  86. package/dist/products/severino/github.js +195 -0
  87. package/dist/products/severino/github.js.map +1 -0
  88. package/dist/products/severino/hostinger.d.ts +140 -0
  89. package/dist/products/severino/hostinger.js +607 -0
  90. package/dist/products/severino/hostinger.js.map +1 -0
  91. package/dist/products/severino/i18n.d.ts +887 -0
  92. package/dist/products/severino/i18n.js +587 -0
  93. package/dist/products/severino/i18n.js.map +1 -0
  94. package/dist/products/severino/keys.d.ts +5 -0
  95. package/dist/products/severino/keys.js +16 -0
  96. package/dist/products/severino/keys.js.map +1 -0
  97. package/dist/products/severino/main.d.ts +3 -0
  98. package/dist/products/severino/main.js +78 -0
  99. package/dist/products/severino/main.js.map +1 -0
  100. package/dist/products/severino/recovery.d.ts +50 -0
  101. package/dist/products/severino/recovery.js +96 -0
  102. package/dist/products/severino/recovery.js.map +1 -0
  103. package/dist/products/severino/sealed-box.d.ts +1 -0
  104. package/dist/products/severino/sealed-box.js +11 -0
  105. package/dist/products/severino/sealed-box.js.map +1 -0
  106. package/dist/products/severino/server.d.ts +123 -0
  107. package/dist/products/severino/server.js +274 -0
  108. package/dist/products/severino/server.js.map +1 -0
  109. package/dist/products/severino/service.d.ts +28 -0
  110. package/dist/products/severino/service.js +752 -0
  111. package/dist/products/severino/service.js.map +1 -0
  112. package/dist/products/severino/slack-manifest.d.ts +3 -0
  113. package/dist/products/severino/slack-manifest.js +45 -0
  114. package/dist/products/severino/slack-manifest.js.map +1 -0
  115. package/dist/products/severino/ssh.d.ts +35 -0
  116. package/dist/products/severino/ssh.js +140 -0
  117. package/dist/products/severino/ssh.js.map +1 -0
  118. package/dist/products/severino/state.d.ts +26 -0
  119. package/dist/products/severino/state.js +61 -0
  120. package/dist/products/severino/state.js.map +1 -0
  121. package/dist/products/severino/ui/assets/index-7S8Hl4jq.js +96 -0
  122. package/dist/products/severino/ui/assets/index-D5kql1Bh.css +2 -0
  123. package/dist/products/severino/ui/index.html +15 -0
  124. package/dist/products/severino/verify.d.ts +22 -0
  125. package/dist/products/severino/verify.js +149 -0
  126. package/dist/products/severino/verify.js.map +1 -0
  127. package/package.json +53 -0
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Deploy keypair generation. ssh2 emits OpenSSH-format ed25519 keys on every
3
+ * platform, which is what the workflow writes verbatim to ~/.ssh/deploy_key.
4
+ */
5
+ import ssh2 from "ssh2";
6
+ export function generateDeployKey() {
7
+ return new Promise((resolve, reject) => {
8
+ ssh2.utils.generateKeyPair("ed25519", { comment: "second-brain-deploy" }, (error, keys) => {
9
+ if (error)
10
+ reject(error);
11
+ else
12
+ resolve({ publicKey: keys.public, privateKey: keys.private });
13
+ });
14
+ });
15
+ }
16
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../packages/second-brain/src/keys.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,IAAI,MAAM,MAAM,CAAC;AAOxB,MAAM,UAAU,iBAAiB;IAC/B,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,CAAC,KAAK,CAAC,eAAe,CACxB,SAAS,EACT,EAAE,OAAO,EAAE,qBAAqB,EAAE,EAClC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;YACd,IAAI,KAAK;gBAAE,MAAM,CAAC,KAAK,CAAC,CAAC;;gBACpB,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QACrE,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,3 @@
1
+ export declare function runSecondBrainInstaller(options: {
2
+ readonly projectRoot: string;
3
+ }): Promise<void>;
@@ -0,0 +1,78 @@
1
+ /** Local web-UI installer composition root for the Second Brain product. */
2
+ import { execFile } from "node:child_process";
3
+ import { spawn } from "node:child_process";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { parseRepositoryFromRemote } from "./github.js";
7
+ import { defaultLanguage, installerMessages } from "./i18n.js";
8
+ import { startInstallerServer } from "./server.js";
9
+ import { createInstallerService, loadInitialState } from "./service.js";
10
+ import { stateFileName } from "./state.js";
11
+ const execFileAsync = promisify(execFile);
12
+ const messages = installerMessages[defaultLanguage];
13
+ async function detectRepository(repoRoot) {
14
+ try {
15
+ const { stdout } = await execFileAsync("git", ["remote", "get-url", "origin"], { cwd: repoRoot });
16
+ return parseRepositoryFromRemote(stdout.trim());
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function openBrowser(url) {
23
+ const command = process.platform === "darwin"
24
+ ? { file: "open", args: [url] }
25
+ : process.platform === "win32"
26
+ ? { file: "cmd", args: ["/c", "start", "", url] }
27
+ : { file: "xdg-open", args: [url] };
28
+ try {
29
+ const child = spawn(command.file, command.args, {
30
+ detached: true,
31
+ stdio: "ignore",
32
+ });
33
+ child.on("error", () => { });
34
+ child.unref();
35
+ }
36
+ catch {
37
+ // The printed URL is the contract; opening the browser is best effort.
38
+ }
39
+ }
40
+ export async function runSecondBrainInstaller(options) {
41
+ const repoRoot = path.resolve(options.projectRoot);
42
+ const statePath = path.join(repoRoot, stateFileName);
43
+ let server;
44
+ try {
45
+ const [initialState, detectedRepository] = await Promise.all([
46
+ loadInitialState(statePath),
47
+ detectRepository(repoRoot),
48
+ ]);
49
+ const service = createInstallerService({
50
+ statePath,
51
+ repoRoot,
52
+ initialState,
53
+ detectedRepository,
54
+ });
55
+ server = await startInstallerServer(service);
56
+ }
57
+ catch {
58
+ throw new Error(messages.terminalStartFailed);
59
+ }
60
+ process.stdout.write(`${messages.terminalRunningAt}\n│ ${server.url}\n│\n`);
61
+ process.stdout.write(`${messages.terminalPressCtrlC}\n`);
62
+ openBrowser(server.url);
63
+ let shutdownPromise = null;
64
+ const shutdown = () => {
65
+ if (shutdownPromise !== null)
66
+ return;
67
+ shutdownPromise = server
68
+ .close()
69
+ .then(() => process.exit(0))
70
+ .catch(() => {
71
+ process.stderr.write(`${messages.terminalStopFailed}\n`);
72
+ process.exit(1);
73
+ });
74
+ };
75
+ process.on("SIGINT", shutdown);
76
+ process.on("SIGTERM", shutdown);
77
+ }
78
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../../packages/second-brain/src/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Host-port provisioning for the compose loopback binds. The compose file
3
+ * publishes the engine on 127.0.0.1:3000 and the OAuth server on
4
+ * 127.0.0.1:3001 by default, and another product installed on the same VPS
5
+ * may already hold those ports — `docker compose up` then fails with "port
6
+ * is already allocated". The installer probes the server's listeners and
7
+ * provisions free ports through SECOND_BRAIN_HTTP_PORT and
8
+ * SECOND_BRAIN_OAUTH_PORT in /opt/second-brain/.env.
9
+ */
10
+ export declare const defaultHttpPort = 3000;
11
+ export declare const defaultOauthPort = 3001;
12
+ /**
13
+ * Lists every TCP listener, then the host ports second-brain's own
14
+ * containers publish. Own binds are not conflicts: compose replaces those
15
+ * containers and rebinds the same ports. Each half degrades to empty output
16
+ * instead of failing the install on a server missing one command.
17
+ */
18
+ export declare const portProbeCommand: string;
19
+ /** Ports something other than second-brain's own containers listens on. */
20
+ export declare function parsePortProbe(stdout: string): Set<number>;
21
+ export interface HostPorts {
22
+ readonly httpPort: number;
23
+ readonly oauthPort: number;
24
+ }
25
+ export declare function isDefaultHostPorts(ports: HostPorts): boolean;
26
+ /**
27
+ * Keeps ports a previous install pinned in .env (its own containers hold
28
+ * those binds) and otherwise picks the first free port at or above each
29
+ * default. The engine skips the OAuth default so the pair stays legible.
30
+ */
31
+ export declare function chooseHostPorts(input: {
32
+ readonly existingEnv: Record<string, string>;
33
+ readonly occupied: ReadonlySet<number>;
34
+ }): HostPorts;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Host-port provisioning for the compose loopback binds. The compose file
3
+ * publishes the engine on 127.0.0.1:3000 and the OAuth server on
4
+ * 127.0.0.1:3001 by default, and another product installed on the same VPS
5
+ * may already hold those ports — `docker compose up` then fails with "port
6
+ * is already allocated". The installer probes the server's listeners and
7
+ * provisions free ports through SECOND_BRAIN_HTTP_PORT and
8
+ * SECOND_BRAIN_OAUTH_PORT in /opt/second-brain/.env.
9
+ */
10
+ export const defaultHttpPort = 3000;
11
+ export const defaultOauthPort = 3001;
12
+ const probeSeparator = "__second-brain-own-ports__";
13
+ const scanLimit = 200;
14
+ /**
15
+ * Lists every TCP listener, then the host ports second-brain's own
16
+ * containers publish. Own binds are not conflicts: compose replaces those
17
+ * containers and rebinds the same ports. Each half degrades to empty output
18
+ * instead of failing the install on a server missing one command.
19
+ */
20
+ export const portProbeCommand = `ss -ltnH 2>/dev/null; echo ${probeSeparator}; ` +
21
+ "docker ps --filter label=com.docker.compose.project=second-brain " +
22
+ "--format '{{.Ports}}' 2>/dev/null; true";
23
+ function parsePort(value) {
24
+ if (value === undefined || !/^\d+$/.test(value))
25
+ return undefined;
26
+ const port = Number(value);
27
+ return port >= 1 && port <= 65535 ? port : undefined;
28
+ }
29
+ /** Ports something other than second-brain's own containers listens on. */
30
+ export function parsePortProbe(stdout) {
31
+ const [listenersText = "", ownText = ""] = stdout.split(probeSeparator);
32
+ const occupied = new Set();
33
+ for (const line of listenersText.split("\n")) {
34
+ // ss -ltnH: "LISTEN 0 4096 127.0.0.1:3000 0.0.0.0:*"; the local address
35
+ // may also be [::]:3000 or *:3000, so take the digits after the last ":".
36
+ const local = line.trim().split(/\s+/)[3];
37
+ const port = parsePort(local?.slice(local.lastIndexOf(":") + 1));
38
+ if (port !== undefined)
39
+ occupied.add(port);
40
+ }
41
+ // docker ps Ports: "127.0.0.1:3001->3001/tcp, 3000/tcp" — the host port
42
+ // sits between ":" and "->".
43
+ for (const match of ownText.matchAll(/:(\d+)->/g)) {
44
+ const own = parsePort(match[1]);
45
+ if (own !== undefined)
46
+ occupied.delete(own);
47
+ }
48
+ return occupied;
49
+ }
50
+ export function isDefaultHostPorts(ports) {
51
+ return (ports.httpPort === defaultHttpPort && ports.oauthPort === defaultOauthPort);
52
+ }
53
+ /**
54
+ * Keeps ports a previous install pinned in .env (its own containers hold
55
+ * those binds) and otherwise picks the first free port at or above each
56
+ * default. The engine skips the OAuth default so the pair stays legible.
57
+ */
58
+ export function chooseHostPorts(input) {
59
+ const pinnedHttp = parsePort(input.existingEnv["SECOND_BRAIN_HTTP_PORT"]);
60
+ const pinnedOauth = parsePort(input.existingEnv["SECOND_BRAIN_OAUTH_PORT"]);
61
+ const taken = new Set(input.occupied);
62
+ if (pinnedOauth !== undefined)
63
+ taken.add(pinnedOauth);
64
+ const httpPort = pinnedHttp ??
65
+ nextFreePort(defaultHttpPort, new Set([...taken, defaultOauthPort]));
66
+ taken.add(httpPort);
67
+ const oauthPort = pinnedOauth ?? nextFreePort(defaultOauthPort, taken);
68
+ return { httpPort, oauthPort };
69
+ }
70
+ function nextFreePort(from, taken) {
71
+ for (let port = from; port < from + scanLimit; port += 1) {
72
+ if (!taken.has(port))
73
+ return port;
74
+ }
75
+ throw new Error(`No free port found between ${from} and ${from + scanLimit - 1}.`);
76
+ }
77
+ //# sourceMappingURL=ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports.js","sourceRoot":"","sources":["../../../packages/second-brain/src/ports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Reconnects a new computer to an existing VPS installation. Recovery proves
3
+ * server ownership over SSH, reads only the public hostname, installs a fresh
4
+ * deploy key for this computer, and persists a new private local state file.
5
+ */
6
+ import type { DeployKeyPair } from "./keys.js";
7
+ import type { InstallerLanguage } from "./i18n.js";
8
+ import type { InstallerState } from "./state.js";
9
+ import type { SshLike } from "./apply.js";
10
+ type RecoverySshAuth = {
11
+ readonly kind: "password";
12
+ readonly password: string;
13
+ } | {
14
+ readonly kind: "key";
15
+ readonly privateKey: string;
16
+ };
17
+ export interface RecoveryInput {
18
+ readonly language: InstallerLanguage;
19
+ readonly repository?: {
20
+ readonly owner: string;
21
+ readonly repo: string;
22
+ } | undefined;
23
+ readonly vps: {
24
+ readonly host: string;
25
+ readonly port: number;
26
+ readonly username: string;
27
+ };
28
+ readonly vpsAuth: RecoverySshAuth;
29
+ }
30
+ export interface RecoveryDependencies {
31
+ readonly generateKey: () => Promise<DeployKeyPair>;
32
+ readonly connectSsh: (credentials: {
33
+ readonly host: string;
34
+ readonly port: number;
35
+ readonly username: string;
36
+ readonly auth: RecoverySshAuth;
37
+ }) => Promise<SshLike>;
38
+ readonly saveState: (state: InstallerState) => Promise<void>;
39
+ readonly now: () => Date;
40
+ }
41
+ export type RecoveryOutcome = {
42
+ readonly ok: true;
43
+ readonly domain: string;
44
+ } | {
45
+ readonly ok: false;
46
+ readonly error: string;
47
+ };
48
+ export declare const readRecoveryMetadataCommand: string;
49
+ export declare function recoverExistingInstallation(input: RecoveryInput, deps: RecoveryDependencies): Promise<RecoveryOutcome>;
50
+ export {};
@@ -0,0 +1,96 @@
1
+ import { normalizeDomain } from "./envfile.js";
2
+ import { buildAuthorizedKeysCommand } from "./ssh.js";
3
+ export const readRecoveryMetadataCommand = [
4
+ "test -f /opt/second-brain/.env || exit 44",
5
+ "test -f /opt/second-brain/compose.yaml || exit 44",
6
+ "domain=$(awk -F= '$1 == \"SECOND_BRAIN_PUBLIC_HOST\" { sub(/^[^=]*=/, \"\"); print; exit }' /opt/second-brain/.env)",
7
+ "test -n \"$domain\" || exit 45",
8
+ "printf '%s\\n' \"$domain\"",
9
+ ].join(" && ");
10
+ export async function recoverExistingInstallation(input, deps) {
11
+ let keys;
12
+ try {
13
+ keys = await deps.generateKey();
14
+ }
15
+ catch {
16
+ return {
17
+ ok: false,
18
+ error: "A new local deploy key could not be created.",
19
+ };
20
+ }
21
+ let ssh;
22
+ try {
23
+ ssh = await deps.connectSsh({ ...input.vps, auth: input.vpsAuth });
24
+ }
25
+ catch (error) {
26
+ return {
27
+ ok: false,
28
+ error: error instanceof Error ? error.message : String(error),
29
+ };
30
+ }
31
+ try {
32
+ const metadata = await ssh.exec(readRecoveryMetadataCommand);
33
+ if (metadata.code !== 0 || metadata.stdout.trim() === "") {
34
+ return {
35
+ ok: false,
36
+ error: "No existing Second Brain installation was found on this server.",
37
+ };
38
+ }
39
+ let domain;
40
+ try {
41
+ domain = normalizeDomain(metadata.stdout.trim());
42
+ }
43
+ catch {
44
+ return {
45
+ ok: false,
46
+ error: "The existing installation has an invalid public hostname.",
47
+ };
48
+ }
49
+ const authorized = await ssh.exec(buildAuthorizedKeysCommand(keys.publicKey));
50
+ if (authorized.code !== 0) {
51
+ return {
52
+ ok: false,
53
+ error: "The new local deploy key could not be installed on the server.",
54
+ };
55
+ }
56
+ try {
57
+ const keySession = await deps.connectSsh({
58
+ ...input.vps,
59
+ auth: { kind: "key", privateKey: keys.privateKey },
60
+ });
61
+ keySession.close();
62
+ }
63
+ catch {
64
+ return {
65
+ ok: false,
66
+ error: "The new local deploy key could not connect to the server.",
67
+ };
68
+ }
69
+ const timestamp = deps.now().toISOString();
70
+ try {
71
+ await deps.saveState({
72
+ version: 1,
73
+ language: input.language,
74
+ ...(input.repository === undefined
75
+ ? {}
76
+ : { repository: input.repository }),
77
+ vps: input.vps,
78
+ deployKey: keys,
79
+ domain,
80
+ installedAt: timestamp,
81
+ updatedAt: timestamp,
82
+ });
83
+ }
84
+ catch {
85
+ return {
86
+ ok: false,
87
+ error: "The recovered settings could not be saved on this computer.",
88
+ };
89
+ }
90
+ return { ok: true, domain };
91
+ }
92
+ finally {
93
+ ssh.close();
94
+ }
95
+ }
96
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.js","sourceRoot":"","sources":["../../../packages/second-brain/src/recovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export declare function sealForGitHubSecret(publicKeyBase64: string, value: string): Promise<string>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * GitHub Actions secrets require libsodium sealed-box encryption with the
3
+ * repository public key; node:crypto has no crypto_box_seal equivalent.
4
+ */
5
+ import sodium from "libsodium-wrappers";
6
+ export async function sealForGitHubSecret(publicKeyBase64, value) {
7
+ await sodium.ready;
8
+ const sealed = sodium.crypto_box_seal(sodium.from_string(value), sodium.from_base64(publicKeyBase64, sodium.base64_variants.ORIGINAL));
9
+ return sodium.to_base64(sealed, sodium.base64_variants.ORIGINAL);
10
+ }
11
+ //# sourceMappingURL=sealed-box.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealed-box.js","sourceRoot":"","sources":["../../../packages/second-brain/src/sealed-box.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,MAAM,MAAM,oBAAoB,CAAC;AAExC,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,eAAuB,EACvB,KAAa;IAEb,MAAM,MAAM,CAAC,KAAK,CAAC;IACnB,MAAM,MAAM,GAAG,MAAM,CAAC,eAAe,CACnC,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,EACzB,MAAM,CAAC,WAAW,CAAC,eAAe,EAAE,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CACrE,CAAC;IACF,OAAO,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;AACnE,CAAC"}
@@ -0,0 +1,143 @@
1
+ import type { InstallerLanguage } from "./i18n.js";
2
+ import type { InstallStepReport } from "./apply.js";
3
+ import type { DnsCheckResult, VerificationResult } from "./verify.js";
4
+ import type { HostingerVirtualMachine } from "./hostinger.js";
5
+ import type { WorkflowRun } from "./github.js";
6
+ export interface InstallerContext {
7
+ readonly mode: "wizard" | "dashboard";
8
+ readonly language: InstallerLanguage;
9
+ readonly repository: {
10
+ readonly owner: string;
11
+ readonly repo: string;
12
+ } | null;
13
+ readonly domain: string | null;
14
+ readonly vpsHost: string | null;
15
+ readonly ownerEmail: string | null;
16
+ readonly githubConnected: boolean;
17
+ }
18
+ export interface InstallProgress {
19
+ readonly running: boolean;
20
+ readonly finished: boolean;
21
+ readonly ok: boolean | null;
22
+ readonly steps: readonly InstallStepReport[];
23
+ /**
24
+ * The provisioned static bearer token, present only after an install that
25
+ * requested one finished. Held in memory and shown once — never persisted.
26
+ */
27
+ readonly bearerToken?: string;
28
+ }
29
+ interface HostingerOperationFailure {
30
+ readonly ok: false;
31
+ readonly errorCode: string;
32
+ readonly correlationId?: string;
33
+ readonly retryAfterMs?: number;
34
+ }
35
+ type HostingerVirtualMachinesResult = {
36
+ readonly ok: true;
37
+ readonly virtualMachines: readonly HostingerVirtualMachine[];
38
+ } | HostingerOperationFailure;
39
+ type HostingerPasswordPreparationResult = {
40
+ readonly ok: true;
41
+ readonly confirmationToken: string;
42
+ readonly target: {
43
+ readonly virtualMachineId: number;
44
+ readonly hostname: string;
45
+ readonly ipv4Id: number;
46
+ readonly ipv4: string;
47
+ };
48
+ } | HostingerOperationFailure;
49
+ type HostingerPasswordResetResult = {
50
+ readonly ok: true;
51
+ readonly actionId: number;
52
+ } | HostingerOperationFailure;
53
+ export interface InstallerService {
54
+ context(): InstallerContext;
55
+ connectHostinger(input: {
56
+ token: string;
57
+ }): Promise<HostingerVirtualMachinesResult>;
58
+ disconnectHostinger(): Promise<{
59
+ readonly ok: true;
60
+ } | HostingerOperationFailure>;
61
+ refreshHostinger(): Promise<HostingerVirtualMachinesResult>;
62
+ prepareHostingerRootPasswordReset(input: {
63
+ virtualMachineId: number;
64
+ }): Promise<HostingerPasswordPreparationResult>;
65
+ resetHostingerRootPassword(input: {
66
+ confirmationToken: string;
67
+ confirmationText: string;
68
+ password: string;
69
+ }): Promise<HostingerPasswordResetResult>;
70
+ validateGitHub(input: {
71
+ token: string;
72
+ owner: string;
73
+ repo: string;
74
+ }): Promise<unknown>;
75
+ connectGitHub(input: {
76
+ token: string;
77
+ owner?: string;
78
+ repo?: string;
79
+ }): Promise<unknown>;
80
+ testSsh(input: {
81
+ host: string;
82
+ port: number;
83
+ username: string;
84
+ password: string;
85
+ }): Promise<{
86
+ ok: boolean;
87
+ message?: string;
88
+ proof?: string;
89
+ }>;
90
+ recoverExisting(input: Record<string, unknown>): Promise<{
91
+ ok: boolean;
92
+ domain?: string;
93
+ error?: string;
94
+ }>;
95
+ checkDns(input: {
96
+ domain: string;
97
+ host: string;
98
+ }): Promise<DnsCheckResult>;
99
+ startInstall(input: Record<string, unknown>): {
100
+ started: boolean;
101
+ error?: string;
102
+ };
103
+ deployLocal(): {
104
+ started: boolean;
105
+ };
106
+ progress(): InstallProgress;
107
+ deploy(input: {
108
+ imageTag?: string;
109
+ }): Promise<{
110
+ ok: boolean;
111
+ }>;
112
+ listRuns(): Promise<WorkflowRun[]>;
113
+ bootstrapOwner(input: {
114
+ email: string;
115
+ password: string;
116
+ }): Promise<{
117
+ kind: string;
118
+ message?: string;
119
+ }>;
120
+ provisionBearer(input: {
121
+ email: string;
122
+ }): Promise<{
123
+ ok: boolean;
124
+ bearerToken?: string;
125
+ error?: string;
126
+ }>;
127
+ verify(): Promise<VerificationResult>;
128
+ reset(): Promise<{
129
+ ok: boolean;
130
+ errorCode?: string;
131
+ }>;
132
+ shutdown?(): void | Promise<void>;
133
+ }
134
+ export interface InstallerServer {
135
+ readonly url: string;
136
+ close(): Promise<void>;
137
+ }
138
+ export interface InstallerServerOptions {
139
+ /** Directory holding the built UI (index.html + assets/). Test override. */
140
+ readonly uiDir?: string;
141
+ }
142
+ export declare function startInstallerServer(service: InstallerService, options?: InstallerServerOptions): Promise<InstallerServer>;
143
+ export {};