terminay 4.2.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 (79) hide show
  1. package/dist/account.d.ts +31 -0
  2. package/dist/account.js +150 -0
  3. package/dist/account.js.map +1 -0
  4. package/dist/address.d.ts +14 -0
  5. package/dist/address.js +58 -0
  6. package/dist/address.js.map +1 -0
  7. package/dist/args.d.ts +37 -0
  8. package/dist/args.js +252 -0
  9. package/dist/args.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +92 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/commands/install.d.ts +48 -0
  14. package/dist/commands/install.js +213 -0
  15. package/dist/commands/install.js.map +1 -0
  16. package/dist/commands/lifecycle.d.ts +23 -0
  17. package/dist/commands/lifecycle.js +66 -0
  18. package/dist/commands/lifecycle.js.map +1 -0
  19. package/dist/commands/pairing.d.ts +23 -0
  20. package/dist/commands/pairing.js +143 -0
  21. package/dist/commands/pairing.js.map +1 -0
  22. package/dist/commands/uninstall.d.ts +26 -0
  23. package/dist/commands/uninstall.js +70 -0
  24. package/dist/commands/uninstall.js.map +1 -0
  25. package/dist/commands/upgrade.d.ts +30 -0
  26. package/dist/commands/upgrade.js +175 -0
  27. package/dist/commands/upgrade.js.map +1 -0
  28. package/dist/context.d.ts +26 -0
  29. package/dist/context.js +37 -0
  30. package/dist/context.js.map +1 -0
  31. package/dist/download.d.ts +11 -0
  32. package/dist/download.js +95 -0
  33. package/dist/download.js.map +1 -0
  34. package/dist/health.d.ts +18 -0
  35. package/dist/health.js +61 -0
  36. package/dist/health.js.map +1 -0
  37. package/dist/http.d.ts +11 -0
  38. package/dist/http.js +88 -0
  39. package/dist/http.js.map +1 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.js +11 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/install.d.ts +30 -0
  44. package/dist/install.js +100 -0
  45. package/dist/install.js.map +1 -0
  46. package/dist/layout.d.ts +52 -0
  47. package/dist/layout.js +96 -0
  48. package/dist/layout.js.map +1 -0
  49. package/dist/manifest.d.ts +46 -0
  50. package/dist/manifest.js +115 -0
  51. package/dist/manifest.js.map +1 -0
  52. package/dist/platform.d.ts +17 -0
  53. package/dist/platform.js +34 -0
  54. package/dist/platform.js.map +1 -0
  55. package/dist/prompt.d.ts +20 -0
  56. package/dist/prompt.js +47 -0
  57. package/dist/prompt.js.map +1 -0
  58. package/dist/resolve.d.ts +35 -0
  59. package/dist/resolve.js +194 -0
  60. package/dist/resolve.js.map +1 -0
  61. package/dist/socket.d.ts +55 -0
  62. package/dist/socket.js +98 -0
  63. package/dist/socket.js.map +1 -0
  64. package/dist/socketClient.d.ts +1 -0
  65. package/dist/socketClient.js +22 -0
  66. package/dist/socketClient.js.map +1 -0
  67. package/dist/source.d.ts +24 -0
  68. package/dist/source.js +152 -0
  69. package/dist/source.js.map +1 -0
  70. package/dist/systemd.d.ts +30 -0
  71. package/dist/systemd.js +86 -0
  72. package/dist/systemd.js.map +1 -0
  73. package/dist/unit.d.ts +37 -0
  74. package/dist/unit.js +74 -0
  75. package/dist/unit.js.map +1 -0
  76. package/dist/verify.d.ts +24 -0
  77. package/dist/verify.js +46 -0
  78. package/dist/verify.js.map +1 -0
  79. package/package.json +49 -0
package/dist/cli.js ADDED
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { ScopeError } from './account.js';
3
+ import { HELP_TEXT, parseCommandLine, UsageError } from './args.js';
4
+ import { runInstall } from './commands/install.js';
5
+ import { runStart, runStatus, runStop } from './commands/lifecycle.js';
6
+ import { runApprovals, runPairing, runResolveApproval, } from './commands/pairing.js';
7
+ import { runResetIdentity, runUninstall } from './commands/uninstall.js';
8
+ import { runUpgrade } from './commands/upgrade.js';
9
+ import { NotInstalledError, resolveContext } from './context.js';
10
+ import { ManifestError } from './manifest.js';
11
+ import { assertSupportedHost, UnsupportedHostError } from './platform.js';
12
+ import { SocketError } from './socket.js';
13
+ import { VerificationError } from './verify.js';
14
+ /**
15
+ * The `terminay` binary.
16
+ *
17
+ * Every daemon command passes the platform gate first, so an operator who runs
18
+ * this on their laptop is told immediately rather than part-way through an
19
+ * install. Failures print one clear line and exit non-zero; the stack trace is
20
+ * not the operator's problem.
21
+ */
22
+ const write = (line) => process.stdout.write(`${line}\n`);
23
+ async function main(argv) {
24
+ const parsed = parseCommandLine(argv);
25
+ if (parsed.command === 'help') {
26
+ process.stdout.write(HELP_TEXT);
27
+ return 0;
28
+ }
29
+ assertSupportedHost();
30
+ if (parsed.command === 'install') {
31
+ const result = await runInstall(parsed.ref, parsed.options, { write });
32
+ return result.ready ? 0 : 1;
33
+ }
34
+ const context = await resolveContext({ options: parsed.options, write });
35
+ switch (parsed.command) {
36
+ case 'upgrade':
37
+ await runUpgrade(parsed.ref, parsed.options, context);
38
+ return 0;
39
+ case 'start':
40
+ return (await runStart(context)) ? 0 : 1;
41
+ case 'stop':
42
+ await runStop(context);
43
+ return 0;
44
+ case 'status':
45
+ await runStatus(context);
46
+ return 0;
47
+ case 'uninstall':
48
+ await runUninstall(parsed.options, context);
49
+ return 0;
50
+ case 'qr-code':
51
+ await runPairing(parsed.options, context);
52
+ return 0;
53
+ case 'approvals':
54
+ await runApprovals(context);
55
+ return 0;
56
+ case 'approve':
57
+ case 'deny':
58
+ await runResolveApproval(parsed.command, parsed.approvalId, context);
59
+ return 0;
60
+ case 'reset-identity':
61
+ await runResetIdentity(parsed.options, context);
62
+ return 0;
63
+ }
64
+ }
65
+ /** Errors the operator caused or must act on print as one line, not a trace. */
66
+ const EXPECTED_ERRORS = [
67
+ UsageError,
68
+ UnsupportedHostError,
69
+ ScopeError,
70
+ NotInstalledError,
71
+ VerificationError,
72
+ ManifestError,
73
+ SocketError,
74
+ ];
75
+ try {
76
+ process.exitCode = await main(process.argv.slice(2));
77
+ }
78
+ catch (error) {
79
+ const expected = EXPECTED_ERRORS.some((kind) => error instanceof kind) ||
80
+ (error instanceof Error && error.name.endsWith('Error'));
81
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
82
+ if (error instanceof UsageError)
83
+ process.stderr.write('\nRun `terminay --help` for usage.\n');
84
+ if (!expected &&
85
+ error instanceof Error &&
86
+ error.stack !== undefined &&
87
+ process.env.TERMINAY_DEBUG === '1') {
88
+ process.stderr.write(`${error.stack}\n`);
89
+ }
90
+ process.exitCode = 1;
91
+ }
92
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EACN,YAAY,EACZ,UAAU,EACV,kBAAkB,GAClB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;GAOG;AAEH,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC;AAElE,KAAK,UAAU,IAAI,CAAC,IAAuB;IAC1C,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC/B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAChC,OAAO,CAAC,CAAC;IACV,CAAC;IAED,mBAAmB,EAAE,CAAC;IAEtB,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACvE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAEzE,QAAQ,MAAM,CAAC,OAAO,EAAE,CAAC;QACxB,KAAK,SAAS;YACb,MAAM,UAAU,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACtD,OAAO,CAAC,CAAC;QACV,KAAK,OAAO;YACX,OAAO,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1C,KAAK,MAAM;YACV,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;YACvB,OAAO,CAAC,CAAC;QACV,KAAK,QAAQ;YACZ,MAAM,SAAS,CAAC,OAAO,CAAC,CAAC;YACzB,OAAO,CAAC,CAAC;QACV,KAAK,WAAW;YACf,MAAM,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC5C,OAAO,CAAC,CAAC;QACV,KAAK,SAAS;YACb,MAAM,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC1C,OAAO,CAAC,CAAC;QACV,KAAK,WAAW;YACf,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC;YAC5B,OAAO,CAAC,CAAC;QACV,KAAK,SAAS,CAAC;QACf,KAAK,MAAM;YACV,MAAM,kBAAkB,CACvB,MAAM,CAAC,OAAO,EACd,MAAM,CAAC,UAAoB,EAC3B,OAAO,CACP,CAAC;YACF,OAAO,CAAC,CAAC;QACV,KAAK,gBAAgB;YACpB,MAAM,gBAAgB,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAChD,OAAO,CAAC,CAAC;IACX,CAAC;AACF,CAAC;AAED,gFAAgF;AAChF,MAAM,eAAe,GAAG;IACvB,UAAU;IACV,oBAAoB;IACpB,UAAU;IACV,iBAAiB;IACjB,iBAAiB;IACjB,aAAa;IACb,WAAW;CACX,CAAC;AAEF,IAAI,CAAC;IACJ,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACtD,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IAChB,MAAM,QAAQ,GACb,eAAe,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,YAAY,IAAI,CAAC;QACrD,CAAC,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1D,OAAO,CAAC,MAAM,CAAC,KAAK,CACnB,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAC7D,CAAC;IACF,IAAI,KAAK,YAAY,UAAU;QAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC9D,IACC,CAAC,QAAQ;QACT,KAAK,YAAY,KAAK;QACtB,KAAK,CAAC,KAAK,KAAK,SAAS;QACzB,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,GAAG,EACjC,CAAC;QACF,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACtB,CAAC"}
@@ -0,0 +1,48 @@
1
+ import type { DaemonOptions } from '../args.js';
2
+ import { type PromptStreams } from '../prompt.js';
3
+ /**
4
+ * `daemon install` — one command from a clean Linux box to a running,
5
+ * exposed, pairable server.
6
+ *
7
+ * The order matters: resolve, download, verify, unpack, verify again against
8
+ * the manifest, and only then touch systemd. Everything that can fail on the
9
+ * operator's behalf fails before the machine has been changed.
10
+ */
11
+ export declare const DEFAULT_PORT = 8443;
12
+ export declare const DEFAULT_HEALTH_PORT = 8444;
13
+ export declare const DEFAULT_HOSTED_DOMAIN = "terminay.com";
14
+ export declare const DEFAULT_EXPOSE = "hosted,direct";
15
+ export interface InstallDependencies {
16
+ readonly home?: string;
17
+ readonly streams?: PromptStreams;
18
+ readonly env?: Readonly<Record<string, string | undefined>>;
19
+ readonly write?: (line: string) => void;
20
+ readonly repository?: string;
21
+ readonly apiBase?: string;
22
+ readonly webBase?: string;
23
+ readonly architecture?: 'x64' | 'arm64';
24
+ readonly serverId?: string;
25
+ readonly uid?: number;
26
+ readonly readinessTimeoutMs?: number;
27
+ /**
28
+ * Verify against this key instead of the embedded one. A test seam for
29
+ * fixtures signed by a throwaway key; it still demands a valid signature,
30
+ * so it relaxes nothing. The `terminay` binary never passes it.
31
+ */
32
+ readonly releasePublicKeyPem?: string;
33
+ /**
34
+ * Install an archive already on this disk instead of resolving a release.
35
+ * It is treated exactly as a source build is — no publisher, so no
36
+ * signature, and the manifest checked as always — which is what the
37
+ * systemd container smoke installs. The `terminay` binary never passes it.
38
+ */
39
+ readonly localArchivePath?: string;
40
+ }
41
+ export interface InstallResult {
42
+ readonly version: string;
43
+ readonly channel: string;
44
+ readonly revision: string;
45
+ readonly directOrigin?: string;
46
+ readonly ready: boolean;
47
+ }
48
+ export declare function runInstall(ref: string | undefined, options: DaemonOptions, dependencies?: InstallDependencies): Promise<InstallResult>;
@@ -0,0 +1,213 @@
1
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir, hostname } from 'node:os';
3
+ import { dirname } from 'node:path';
4
+ import { assertRootForSystemScope, prepareDataRoot, selectRunAs, selectScope, } from '../account.js';
5
+ import { defaultDirectOrigin } from '../address.js';
6
+ import { discardDownloads, downloadAsset, downloadBytes, downloadText, } from '../download.js';
7
+ import { waitForReady } from '../health.js';
8
+ import { activate, installArchive, installedVersions, retain, } from '../install.js';
9
+ import { installLayout, writeInstallRecord, } from '../layout.js';
10
+ import { hostArchitecture } from '../platform.js';
11
+ import { defaultStreams } from '../prompt.js';
12
+ import { resolveRef } from '../resolve.js';
13
+ import { buildFromSource } from '../source.js';
14
+ import { createSystemd } from '../systemd.js';
15
+ import { parseEnvironmentFile, renderEnvironmentFile, renderUnit, } from '../unit.js';
16
+ import { verifyArchive } from '../verify.js';
17
+ /**
18
+ * `daemon install` — one command from a clean Linux box to a running,
19
+ * exposed, pairable server.
20
+ *
21
+ * The order matters: resolve, download, verify, unpack, verify again against
22
+ * the manifest, and only then touch systemd. Everything that can fail on the
23
+ * operator's behalf fails before the machine has been changed.
24
+ */
25
+ export const DEFAULT_PORT = 8443;
26
+ export const DEFAULT_HEALTH_PORT = 8444;
27
+ export const DEFAULT_HOSTED_DOMAIN = 'terminay.com';
28
+ export const DEFAULT_EXPOSE = 'hosted,direct';
29
+ /** Fetch and verify an archive, or build one when the ref is a source ref. */
30
+ async function stageArchive(resolved, layout, write, publicKeyPem) {
31
+ if (resolved.channel === 'source') {
32
+ write(`Building ${resolved.sourceRef ?? resolved.version} from source. This takes a while.`);
33
+ const built = await buildFromSource({
34
+ ref: resolved.sourceRef ?? resolved.version,
35
+ ...(resolved.revision === undefined
36
+ ? {}
37
+ : { revision: resolved.revision }),
38
+ destination: layout.prefix,
39
+ write,
40
+ });
41
+ return { archivePath: built.archivePath, signed: false };
42
+ }
43
+ const assets = resolved.assets;
44
+ if (assets === undefined)
45
+ throw new Error('the resolved release names no archive to download');
46
+ write(`Downloading ${resolved.channel === 'tag' ? resolved.version : `${resolved.channel} channel`} …`);
47
+ const asset = await downloadAsset(assets.archive, layout.prefix);
48
+ const [sidecar, signature] = await Promise.all([
49
+ downloadText(assets.sha256),
50
+ downloadBytes(assets.signature),
51
+ ]);
52
+ await verifyArchive({
53
+ archivePath: asset.path,
54
+ sidecar,
55
+ signature,
56
+ digest: asset.sha256,
57
+ ...(publicKeyPem === undefined ? {} : { publicKeyPem }),
58
+ });
59
+ write('Checksum and signature verified.');
60
+ return { archivePath: asset.path, signed: true };
61
+ }
62
+ export async function runInstall(ref, options, dependencies = {}) {
63
+ const home = dependencies.home ?? homedir();
64
+ const streams = dependencies.streams ?? defaultStreams();
65
+ const write = dependencies.write ?? ((line) => process.stdout.write(`${line}\n`));
66
+ const scope = await selectScope({
67
+ ...(options.scope === undefined ? {} : { requested: options.scope }),
68
+ streams,
69
+ });
70
+ if (scope === 'system') {
71
+ assertRootForSystemScope(`npx terminay daemon install${ref === undefined ? '' : ` ${ref}`} --system`, dependencies.uid ?? process.getuid?.());
72
+ }
73
+ const selection = await selectRunAs({
74
+ scope,
75
+ ...(options.runAs === undefined ? {} : { requested: options.runAs }),
76
+ streams,
77
+ });
78
+ const layout = installLayout(scope, home);
79
+ const architecture = dependencies.architecture ?? hostArchitecture();
80
+ const resolved = dependencies.localArchivePath === undefined
81
+ ? await resolveRef(ref, {
82
+ architecture,
83
+ ...(dependencies.repository === undefined
84
+ ? {}
85
+ : { repository: dependencies.repository }),
86
+ ...(dependencies.apiBase === undefined
87
+ ? {}
88
+ : { apiBase: dependencies.apiBase }),
89
+ ...(dependencies.webBase === undefined
90
+ ? {}
91
+ : { webBase: dependencies.webBase }),
92
+ })
93
+ : Object.freeze({ channel: 'source', version: 'local' });
94
+ await mkdir(layout.prefix, { recursive: true, mode: 0o755 });
95
+ const staged = dependencies.localArchivePath === undefined
96
+ ? await stageArchive(resolved, layout, write, dependencies.releasePublicKeyPem)
97
+ : { archivePath: dependencies.localArchivePath, signed: false };
98
+ const installed = await installArchive({
99
+ layout,
100
+ archivePath: staged.archivePath,
101
+ channel: resolved.channel,
102
+ expected: {
103
+ channel: resolved.channel,
104
+ architecture,
105
+ ...(resolved.channel === 'tag' ? { version: resolved.version } : {}),
106
+ ...(resolved.revision === undefined
107
+ ? {}
108
+ : { revision: resolved.revision }),
109
+ },
110
+ });
111
+ await discardDownloads(layout.prefix);
112
+ const port = options.port ?? DEFAULT_PORT;
113
+ const dataRoot = layout.defaultDataRoot;
114
+ await prepareDataRoot(dataRoot, selection.runAs, scope);
115
+ // A reinstall keeps the identity paired devices already know.
116
+ const existing = parseEnvironmentFile(await readFile(layout.environmentFile, 'utf8').catch(() => ''));
117
+ const serverId = existing.TERMINAY_SERVER_ID ?? dependencies.serverId ?? hostname();
118
+ const expose = options.expose ?? DEFAULT_EXPOSE;
119
+ const wantsDirect = expose.split(',').includes('direct');
120
+ const directOrigin = wantsDirect
121
+ ? (options.directOrigin ?? (await defaultDirectOrigin(port)))
122
+ : options.directOrigin;
123
+ if (wantsDirect && directOrigin === undefined) {
124
+ throw new Error('direct exposure needs an origin devices can reach, and this machine has no routable address to derive one from. Pass --direct-origin https://<host>:<port>.');
125
+ }
126
+ await mkdir(dirname(layout.environmentFile), {
127
+ recursive: true,
128
+ mode: 0o755,
129
+ });
130
+ await writeFile(layout.environmentFile, renderEnvironmentFile({
131
+ serverId,
132
+ dataRoot,
133
+ projectRoot: options.projectRoot ?? selection.home,
134
+ port,
135
+ healthPort: DEFAULT_HEALTH_PORT,
136
+ expose,
137
+ hostedDomain: options.hostedDomain ?? DEFAULT_HOSTED_DOMAIN,
138
+ ...(directOrigin === undefined ? {} : { directOrigin }),
139
+ uiBundle: `${layout.currentLink}/ui`,
140
+ }), { mode: 0o640 });
141
+ // Readable only by the account the service runs as.
142
+ await chmod(layout.environmentFile, 0o640);
143
+ await activate(layout, installed.name);
144
+ await mkdir(dirname(layout.unitPath), { recursive: true, mode: 0o755 });
145
+ await writeFile(layout.unitPath, renderUnit({
146
+ layout,
147
+ runAs: selection.runAs,
148
+ workingDirectory: options.projectRoot ?? selection.home,
149
+ }), { mode: 0o644 });
150
+ const systemd = createSystemd({
151
+ scope,
152
+ ...(dependencies.env === undefined ? {} : { env: dependencies.env }),
153
+ });
154
+ await systemd.daemonReload();
155
+ if (scope === 'user')
156
+ await systemd.enableLinger(selection.runAs);
157
+ await systemd.enableNow();
158
+ const ready = (await waitForReady(DEFAULT_HEALTH_PORT, dependencies.readinessTimeoutMs)) !== undefined;
159
+ const record = {
160
+ schemaVersion: 1,
161
+ scope,
162
+ channel: resolved.channel,
163
+ version: installed.manifest.version,
164
+ revision: installed.manifest.revision,
165
+ ...(resolved.publishedAt === undefined
166
+ ? {}
167
+ : { publishedAt: resolved.publishedAt }),
168
+ runAs: selection.runAs,
169
+ dataRoot,
170
+ projectRoot: options.projectRoot ?? selection.home,
171
+ port,
172
+ healthPort: DEFAULT_HEALTH_PORT,
173
+ expose,
174
+ hostedDomain: options.hostedDomain ?? DEFAULT_HOSTED_DOMAIN,
175
+ ...(directOrigin === undefined ? {} : { directOrigin }),
176
+ installedAt: new Date().toISOString(),
177
+ };
178
+ await writeInstallRecord(layout, record);
179
+ await retain(layout, [
180
+ installed.name,
181
+ ...(await installedVersions(layout))
182
+ .filter((name) => name !== installed.name)
183
+ .slice(-1),
184
+ ]);
185
+ write('');
186
+ write(`Terminay Server ${installed.manifest.version} is installed and ${ready ? 'running' : 'starting'}.`);
187
+ write(` scope ${scope}`);
188
+ write(` runs as ${selection.runAs}`);
189
+ write(` channel ${resolved.channel}`);
190
+ write(` data root ${dataRoot}`);
191
+ write(` exposure ${expose}`);
192
+ if (directOrigin !== undefined) {
193
+ write(` direct URL ${directOrigin}`);
194
+ write('');
195
+ write('If devices cannot reach that address — the machine is behind NAT, or has a');
196
+ write('DNS name — reinstall with --direct-origin https://<host>:<port>.');
197
+ }
198
+ if (!ready) {
199
+ write('');
200
+ write('The server has not reported ready yet. Recent log lines:');
201
+ write(await systemd.journal(20));
202
+ }
203
+ write('');
204
+ write('Pair a device with: npx terminay daemon qr-code');
205
+ return Object.freeze({
206
+ version: installed.manifest.version,
207
+ channel: resolved.channel,
208
+ revision: installed.manifest.revision,
209
+ ...(directOrigin === undefined ? {} : { directOrigin }),
210
+ ready,
211
+ });
212
+ }
213
+ //# sourceMappingURL=install.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install.js","sourceRoot":"","sources":["../../src/commands/install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EACN,wBAAwB,EACxB,eAAe,EACf,WAAW,EACX,WAAW,GACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EACN,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,YAAY,GACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,QAAQ,EACR,cAAc,EACd,iBAAiB,EACjB,MAAM,GACN,MAAM,eAAe,CAAC;AACvB,OAAO,EAGN,aAAa,EACb,kBAAkB,GAClB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAsB,MAAM,cAAc,CAAC;AAClE,OAAO,EAAoB,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EACN,oBAAoB,EACpB,qBAAqB,EACrB,UAAU,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AACjC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACxC,MAAM,CAAC,MAAM,qBAAqB,GAAG,cAAc,CAAC;AACpD,MAAM,CAAC,MAAM,cAAc,GAAG,eAAe,CAAC;AAqC9C,8EAA8E;AAC9E,KAAK,UAAU,YAAY,CAC1B,QAAqB,EACrB,MAAqB,EACrB,KAA6B,EAC7B,YAAqB;IAErB,IAAI,QAAQ,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACnC,KAAK,CACJ,YAAY,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,OAAO,mCAAmC,CACrF,CAAC;QACF,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC;YACnC,GAAG,EAAE,QAAQ,CAAC,SAAS,IAAI,QAAQ,CAAC,OAAO;YAC3C,GAAG,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS;gBAClC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACnC,WAAW,EAAE,MAAM,CAAC,MAAM;YAC1B,KAAK;SACL,CAAC,CAAC;QACH,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC1D,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,MAAM,KAAK,SAAS;QACvB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACtE,KAAK,CACJ,eAAe,QAAQ,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,UAAU,IAAI,CAChG,CAAC;IACF,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IACjE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC9C,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC;QAC3B,aAAa,CAAC,MAAM,CAAC,SAAS,CAAC;KAC/B,CAAC,CAAC;IACH,MAAM,aAAa,CAAC;QACnB,WAAW,EAAE,KAAK,CAAC,IAAI;QACvB,OAAO;QACP,SAAS;QACT,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;KACvD,CAAC,CAAC;IACH,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAC1C,OAAO,EAAE,WAAW,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,GAAuB,EACvB,OAAsB,EACtB,eAAoC,EAAE;IAEtC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,IAAI,OAAO,EAAE,CAAC;IAC5C,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,IAAI,cAAc,EAAE,CAAC;IACzD,MAAM,KAAK,GACV,YAAY,CAAC,KAAK,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC;IAE7E,MAAM,KAAK,GAAiB,MAAM,WAAW,CAAC;QAC7C,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO;KACP,CAAC,CAAC;IACH,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxB,wBAAwB,CACvB,8BAA8B,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,WAAW,EAC3E,YAAY,CAAC,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CACtC,CAAC;IACH,CAAC;IACD,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC;QACnC,KAAK;QACL,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QACpE,OAAO;KACP,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,YAAY,CAAC,YAAY,IAAI,gBAAgB,EAAE,CAAC;IACrE,MAAM,QAAQ,GACb,YAAY,CAAC,gBAAgB,KAAK,SAAS;QAC1C,CAAC,CAAC,MAAM,UAAU,CAAC,GAAG,EAAE;YACtB,YAAY;YACZ,GAAG,CAAC,YAAY,CAAC,UAAU,KAAK,SAAS;gBACxC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,UAAU,EAAE,YAAY,CAAC,UAAU,EAAE,CAAC;YAC3C,GAAG,CAAC,YAAY,CAAC,OAAO,KAAK,SAAS;gBACrC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,CAAC;YACrC,GAAG,CAAC,YAAY,CAAC,OAAO,KAAK,SAAS;gBACrC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,CAAC;SACrC,CAAC;QACH,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,QAAiB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAEpE,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7D,MAAM,MAAM,GACX,YAAY,CAAC,gBAAgB,KAAK,SAAS;QAC1C,CAAC,CAAC,MAAM,YAAY,CAClB,QAAQ,EACR,MAAM,EACN,KAAK,EACL,YAAY,CAAC,mBAAmB,CAChC;QACF,CAAC,CAAC,EAAE,WAAW,EAAE,YAAY,CAAC,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAClE,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC;QACtC,MAAM;QACN,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,QAAQ,EAAE;YACT,OAAO,EAAE,QAAQ,CAAC,OAAO;YACzB,YAAY;YACZ,GAAG,CAAC,QAAQ,CAAC,OAAO,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpE,GAAG,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS;gBAClC,CAAC,CAAC,EAAE;gBACJ,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,EAAE,CAAC;SACnC;KACD,CAAC,CAAC;IACH,MAAM,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEtC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,YAAY,CAAC;IAC1C,MAAM,QAAQ,GAAG,MAAM,CAAC,eAAe,CAAC;IACxC,MAAM,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAExD,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,oBAAoB,CACpC,MAAM,QAAQ,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAC9D,CAAC;IACF,MAAM,QAAQ,GACb,QAAQ,CAAC,kBAAkB,IAAI,YAAY,CAAC,QAAQ,IAAI,QAAQ,EAAE,CAAC;IAEpE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC;IAChD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzD,MAAM,YAAY,GAAG,WAAW;QAC/B,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,IAAI,CAAC,MAAM,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC;QAC7D,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IACxB,IAAI,WAAW,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACd,6JAA6J,CAC7J,CAAC;IACH,CAAC;IAED,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE;QAC5C,SAAS,EAAE,IAAI;QACf,IAAI,EAAE,KAAK;KACX,CAAC,CAAC;IACH,MAAM,SAAS,CACd,MAAM,CAAC,eAAe,EACtB,qBAAqB,CAAC;QACrB,QAAQ;QACR,QAAQ;QACR,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,SAAS,CAAC,IAAI;QAClD,IAAI;QACJ,UAAU,EAAE,mBAAmB;QAC/B,MAAM;QACN,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,qBAAqB;QAC3D,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;QACvD,QAAQ,EAAE,GAAG,MAAM,CAAC,WAAW,KAAK;KACpC,CAAC,EACF,EAAE,IAAI,EAAE,KAAK,EAAE,CACf,CAAC;IACF,oDAAoD;IACpD,MAAM,KAAK,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;IAE3C,MAAM,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;IAEvC,MAAM,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACxE,MAAM,SAAS,CACd,MAAM,CAAC,QAAQ,EACf,UAAU,CAAC;QACV,MAAM;QACN,KAAK,EAAE,SAAS,CAAC,KAAK;QACtB,gBAAgB,EAAE,OAAO,CAAC,WAAW,IAAI,SAAS,CAAC,IAAI;KACvD,CAAC,EACF,EAAE,IAAI,EAAE,KAAK,EAAE,CACf,CAAC;IAEF,MAAM,OAAO,GAAG,aAAa,CAAC;QAC7B,KAAK;QACL,GAAG,CAAC,YAAY,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,YAAY,CAAC,GAAG,EAAE,CAAC;KACpE,CAAC,CAAC;IACH,MAAM,OAAO,CAAC,YAAY,EAAE,CAAC;IAC7B,IAAI,KAAK,KAAK,MAAM;QAAE,MAAM,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAClE,MAAM,OAAO,CAAC,SAAS,EAAE,CAAC;IAE1B,MAAM,KAAK,GACV,CAAC,MAAM,YAAY,CAClB,mBAAmB,EACnB,YAAY,CAAC,kBAAkB,CAC/B,CAAC,KAAK,SAAS,CAAC;IAElB,MAAM,MAAM,GAAkB;QAC7B,aAAa,EAAE,CAAC;QAChB,KAAK;QACL,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC,OAAO;QACnC,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,QAAQ;QACrC,GAAG,CAAC,QAAQ,CAAC,WAAW,KAAK,SAAS;YACrC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC;QACzC,KAAK,EAAE,SAAS,CAAC,KAAK;QACtB,QAAQ;QACR,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,SAAS,CAAC,IAAI;QAClD,IAAI;QACJ,UAAU,EAAE,mBAAmB;QAC/B,MAAM;QACN,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,qBAAqB;QAC3D,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;QACvD,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACrC,CAAC;IACF,MAAM,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,MAAM,CAAC,MAAM,EAAE;QACpB,SAAS,CAAC,IAAI;QACd,GAAG,CAAC,MAAM,iBAAiB,CAAC,MAAM,CAAC,CAAC;aAClC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC;aACzC,KAAK,CAAC,CAAC,CAAC,CAAC;KACX,CAAC,CAAC;IAEH,KAAK,CAAC,EAAE,CAAC,CAAC;IACV,KAAK,CACJ,mBAAmB,SAAS,CAAC,QAAQ,CAAC,OAAO,qBAAqB,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,GAAG,CACnG,CAAC;IACF,KAAK,CAAC,kBAAkB,KAAK,EAAE,CAAC,CAAC;IACjC,KAAK,CAAC,kBAAkB,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC;IAC3C,KAAK,CAAC,kBAAkB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5C,KAAK,CAAC,kBAAkB,QAAQ,EAAE,CAAC,CAAC;IACpC,KAAK,CAAC,kBAAkB,MAAM,EAAE,CAAC,CAAC;IAClC,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;QAChC,KAAK,CAAC,kBAAkB,YAAY,EAAE,CAAC,CAAC;QACxC,KAAK,CAAC,EAAE,CAAC,CAAC;QACV,KAAK,CACJ,4EAA4E,CAC5E,CAAC;QACF,KAAK,CAAC,kEAAkE,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,KAAK,CAAC,EAAE,CAAC,CAAC;QACV,KAAK,CAAC,0DAA0D,CAAC,CAAC;QAClE,KAAK,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,KAAK,CAAC,EAAE,CAAC,CAAC;IACV,KAAK,CAAC,kDAAkD,CAAC,CAAC;IAE1D,OAAO,MAAM,CAAC,MAAM,CAAC;QACpB,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC,OAAO;QACnC,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,QAAQ;QACrC,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC;QACvD,KAAK;KACL,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { CommandContext } from '../context.js';
2
+ /**
3
+ * `daemon start`, `daemon stop`, and `daemon status`.
4
+ *
5
+ * `status` is deliberately thin. It reports the unit, the version, the channel,
6
+ * readiness, and which exposure modes are on — and nothing else. Workspace
7
+ * names, project paths, and device records are the operator's data, not
8
+ * service metadata, and a status command is exactly the sort of thing that
9
+ * ends up pasted into an issue.
10
+ */
11
+ export declare function runStart(context: CommandContext): Promise<boolean>;
12
+ export declare function runStop(context: CommandContext): Promise<void>;
13
+ export interface StatusReport {
14
+ readonly unit: string;
15
+ readonly enabled: boolean;
16
+ readonly version: string;
17
+ readonly channel: string;
18
+ readonly revision: string;
19
+ readonly ready: boolean;
20
+ readonly exposure: readonly string[] | 'off';
21
+ readonly activeVersion?: string;
22
+ }
23
+ export declare function runStatus(context: CommandContext): Promise<StatusReport>;
@@ -0,0 +1,66 @@
1
+ import { probeHealth, waitForReady } from '../health.js';
2
+ import { activeVersion } from '../install.js';
3
+ import { approvalSocketPath, requireOk, sendAsUser } from '../socket.js';
4
+ /**
5
+ * `daemon start`, `daemon stop`, and `daemon status`.
6
+ *
7
+ * `status` is deliberately thin. It reports the unit, the version, the channel,
8
+ * readiness, and which exposure modes are on — and nothing else. Workspace
9
+ * names, project paths, and device records are the operator's data, not
10
+ * service metadata, and a status command is exactly the sort of thing that
11
+ * ends up pasted into an issue.
12
+ */
13
+ export async function runStart(context) {
14
+ await context.systemd.start();
15
+ const snapshot = await waitForReady(context.record.healthPort);
16
+ if (snapshot !== undefined) {
17
+ context.write(`Terminay Server ${snapshot.version ?? context.record.version} is ready.`);
18
+ return true;
19
+ }
20
+ context.write('The server started but has not reported ready. Recent log lines:');
21
+ context.write(await context.systemd.journal(20));
22
+ return false;
23
+ }
24
+ export async function runStop(context) {
25
+ await context.systemd.stop();
26
+ context.write('Terminay Server is stopped.');
27
+ }
28
+ export async function runStatus(context) {
29
+ const [unit, enabled, snapshot, active] = await Promise.all([
30
+ context.systemd.state(),
31
+ context.systemd.isEnabled(),
32
+ probeHealth(context.record.healthPort),
33
+ activeVersion(context.layout),
34
+ ]);
35
+ // Exposure is asked of the running server rather than read from the record,
36
+ // so a hand-edited environment file cannot make the status lie.
37
+ let exposure = context.record.expose
38
+ .split(',')
39
+ .filter((mode) => mode.length > 0);
40
+ try {
41
+ const response = requireOk(await sendAsUser(approvalSocketPath(context.record.dataRoot), { op: 'pairing' }, context.record.runAs));
42
+ if ('exposure' in response)
43
+ exposure = response.exposure;
44
+ }
45
+ catch {
46
+ // A stopped server cannot answer; the configured modes stand.
47
+ }
48
+ const report = Object.freeze({
49
+ unit,
50
+ enabled,
51
+ version: context.record.version,
52
+ channel: context.record.channel,
53
+ revision: context.record.revision,
54
+ ready: snapshot?.ready === true,
55
+ exposure,
56
+ ...(active === undefined ? {} : { activeVersion: active }),
57
+ });
58
+ context.write(`unit ${report.unit}${report.enabled ? ' (enabled)' : ' (not enabled)'}`);
59
+ context.write(`version ${report.version}`);
60
+ context.write(`channel ${report.channel}`);
61
+ context.write(`revision ${report.revision.slice(0, 12)}`);
62
+ context.write(`ready ${report.ready ? 'yes' : 'no'}`);
63
+ context.write(`exposure ${report.exposure === 'off' ? 'off' : report.exposure.join(', ') || 'off'}`);
64
+ return report;
65
+ }
66
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/commands/lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEzE;;;;;;;;GAQG;AAEH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,OAAuB;IACrD,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IAC9B,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC/D,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC5B,OAAO,CAAC,KAAK,CACZ,mBAAmB,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,YAAY,CACzE,CAAC;QACF,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,CAAC,KAAK,CACZ,kEAAkE,CAClE,CAAC;IACF,OAAO,CAAC,KAAK,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,OAAO,KAAK,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,OAAuB;IACpD,MAAM,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;AAC9C,CAAC;AAaD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,OAAuB;IAEvB,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3D,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE;QACvB,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE;QAC3B,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC;QACtC,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC;KAC7B,CAAC,CAAC;IAEH,4EAA4E;IAC5E,gEAAgE;IAChE,IAAI,QAAQ,GAA8B,OAAO,CAAC,MAAM,CAAC,MAAM;SAC7D,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACpC,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,SAAS,CACzB,MAAM,UAAU,CACf,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC3C,EAAE,EAAE,EAAE,SAAS,EAAE,EACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CACpB,CACD,CAAC;QACF,IAAI,UAAU,IAAI,QAAQ;YAAE,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACR,8DAA8D;IAC/D,CAAC;IAED,MAAM,MAAM,GAAiB,MAAM,CAAC,MAAM,CAAC;QAC1C,IAAI;QACJ,OAAO;QACP,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;QAC/B,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;QAC/B,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ;QACjC,KAAK,EAAE,QAAQ,EAAE,KAAK,KAAK,IAAI;QAC/B,QAAQ;QACR,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC;KAC1D,CAAC,CAAC;IAEH,OAAO,CAAC,KAAK,CACZ,gBAAgB,MAAM,CAAC,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,gBAAgB,EAAE,CAChF,CAAC;IACF,OAAO,CAAC,KAAK,CAAC,gBAAgB,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,KAAK,CAAC,gBAAgB,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,KAAK,CAAC,gBAAgB,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9D,OAAO,CAAC,KAAK,CAAC,gBAAgB,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,KAAK,CACZ,gBAAgB,MAAM,CAAC,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,EAAE,CACzF,CAAC;IACF,OAAO,MAAM,CAAC;AACf,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { DaemonOptions } from '../args.js';
2
+ import type { CommandContext } from '../context.js';
3
+ import { type PromptStreams } from '../prompt.js';
4
+ import { type ApprovalResponse, type PendingApproval, sendAsUser } from '../socket.js';
5
+ export declare class PairingError extends Error {
6
+ }
7
+ export interface PairingDependencies {
8
+ readonly streams?: PromptStreams;
9
+ readonly pollIntervalMs?: number;
10
+ readonly now?: () => number;
11
+ readonly renderQr?: (url: string) => Promise<string>;
12
+ readonly send?: (request: Parameters<typeof sendAsUser>[1]) => Promise<ApprovalResponse>;
13
+ }
14
+ export interface PairingResult {
15
+ readonly printed: readonly string[];
16
+ readonly waited: boolean;
17
+ readonly outcome?: 'approved' | 'denied';
18
+ readonly deviceName?: string;
19
+ }
20
+ export declare function runPairing(options: DaemonOptions, context: CommandContext, dependencies?: PairingDependencies): Promise<PairingResult>;
21
+ /** `daemon approvals`, `approve <id>`, and `deny <id>` for scripted use. */
22
+ export declare function runApprovals(context: CommandContext, dependencies?: PairingDependencies): Promise<readonly PendingApproval[]>;
23
+ export declare function runResolveApproval(action: 'approve' | 'deny', approvalId: string, context: CommandContext, dependencies?: PairingDependencies): Promise<void>;
@@ -0,0 +1,143 @@
1
+ import QRCode from 'qrcode';
2
+ import { confirm, defaultStreams, isInteractive, } from '../prompt.js';
3
+ import { approvalSocketPath, requireOk, sendAsUser, } from '../socket.js';
4
+ /**
5
+ * `daemon qr-code` — pairing and approval on one screen.
6
+ *
7
+ * The runbook's version of this is: read the journal for a URL, type it into a
8
+ * device, read the match code off the device, then run `approve <id>` in
9
+ * another terminal before the room expires. This collapses that into one
10
+ * command without moving where the decision happens: the match code is still
11
+ * compared by a human on the host, and approval still goes to the server over
12
+ * its owner-only socket. Nothing here can approve a device on its own.
13
+ *
14
+ * Neither a host key nor a device key is ever printed. The pairing URL's
15
+ * fragment carries a one-time secret and is meant to be shown; a key is not.
16
+ */
17
+ const POLL_INTERVAL_MS = 1_000;
18
+ const REFRESH_WINDOW_MS = 30_000;
19
+ export class PairingError extends Error {
20
+ }
21
+ async function renderTerminalQr(url) {
22
+ return QRCode.toString(url, { type: 'terminal', small: true });
23
+ }
24
+ function selectHandoff(handoffs, mode) {
25
+ if (mode !== undefined)
26
+ return handoffs.find((handoff) => handoff.mode === mode);
27
+ // Direct is preferred when it is on: it reaches the server without a relay,
28
+ // which is the point of enabling it.
29
+ return handoffs.find((handoff) => handoff.mode === 'direct') ?? handoffs[0];
30
+ }
31
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
32
+ export async function runPairing(options, context, dependencies = {}) {
33
+ const { record, write } = context;
34
+ const streams = dependencies.streams ?? defaultStreams();
35
+ const now = dependencies.now ?? Date.now;
36
+ const renderQr = dependencies.renderQr ?? renderTerminalQr;
37
+ const socketPath = approvalSocketPath(record.dataRoot);
38
+ const send = dependencies.send ??
39
+ ((request) => sendAsUser(socketPath, request, record.runAs));
40
+ const fetchPairing = async (rotate) => {
41
+ const response = requireOk(await send(rotate ? { op: 'pairing', rotate: true } : { op: 'pairing' }));
42
+ if (!('handoffs' in response))
43
+ throw new PairingError('the server did not return any pairing information');
44
+ if (response.exposure === 'off' || response.handoffs.length === 0) {
45
+ throw new PairingError('this server is not exposed, so there is no pairing URL to show. Reinstall with --expose hosted,direct, or edit the environment file and restart it.');
46
+ }
47
+ return response;
48
+ };
49
+ let pairing = await fetchPairing(false);
50
+ const printed = [];
51
+ const show = async () => {
52
+ const chosen = selectHandoff(pairing.handoffs, options.mode);
53
+ if (chosen === undefined) {
54
+ throw new PairingError(`this server does not expose a ${options.mode} pairing URL. Enabled modes: ${pairing.exposure === 'off' ? 'none' : pairing.exposure.join(', ')}.`);
55
+ }
56
+ write(await renderQr(chosen.pairingUrl));
57
+ write(`Scan this to pair, or open the ${chosen.mode} URL below.`);
58
+ write('');
59
+ for (const handoff of pairing.handoffs) {
60
+ write(` ${handoff.mode.padEnd(7)} ${handoff.pairingUrl}`);
61
+ write(` ${''.padEnd(7)} expires ${handoff.pairingExpiresAt}`);
62
+ printed.push(handoff.pairingUrl);
63
+ }
64
+ write('');
65
+ };
66
+ await show();
67
+ if (!options.wait)
68
+ return Object.freeze({ printed, waited: false });
69
+ if (!isInteractive(streams)) {
70
+ write('No terminal is attached, so there is nothing to approve on. Use `daemon approvals` and `daemon approve <id>`.');
71
+ return Object.freeze({ printed, waited: false });
72
+ }
73
+ write('Waiting for a device to scan it. Press Ctrl+C to stop.');
74
+ for (;;) {
75
+ const listed = requireOk(await send({ op: 'list' }));
76
+ const pending = 'pending' in listed ? listed.pending : [];
77
+ const first = pending[0];
78
+ if (first !== undefined) {
79
+ write('');
80
+ write(`Device: ${first.deviceName}`);
81
+ write(`Match code: ${first.matchCode}`);
82
+ write('');
83
+ write('Approve this device only if that code is the one shown on it.');
84
+ const approved = await confirm('Approve?', streams);
85
+ const outcome = requireOk(await send(approved
86
+ ? { op: 'approve', approvalId: first.approvalId }
87
+ : { op: 'deny', approvalId: first.approvalId }));
88
+ const resolved = 'outcome' in outcome
89
+ ? outcome.outcome
90
+ : approved
91
+ ? 'approved'
92
+ : 'denied';
93
+ write(resolved === 'approved'
94
+ ? `${first.deviceName} is paired.`
95
+ : `${first.deviceName} was denied.`);
96
+ return Object.freeze({
97
+ printed,
98
+ waited: true,
99
+ outcome: resolved,
100
+ deviceName: first.deviceName,
101
+ });
102
+ }
103
+ // A room that expires while nobody is looking would leave a QR on
104
+ // screen that no longer works, so it is replaced before that happens.
105
+ const soonest = Math.min(...pairing.handoffs
106
+ .map((handoff) => Date.parse(handoff.pairingExpiresAt))
107
+ .filter((value) => Number.isFinite(value)));
108
+ if (Number.isFinite(soonest) && soonest - now() < REFRESH_WINDOW_MS) {
109
+ pairing = await fetchPairing(true);
110
+ write('');
111
+ write('That pairing code expired. Here is a fresh one:');
112
+ write('');
113
+ await show();
114
+ write('Waiting for a device to scan it. Press Ctrl+C to stop.');
115
+ }
116
+ await sleep(dependencies.pollIntervalMs ?? POLL_INTERVAL_MS);
117
+ }
118
+ }
119
+ /** `daemon approvals`, `approve <id>`, and `deny <id>` for scripted use. */
120
+ export async function runApprovals(context, dependencies = {}) {
121
+ const socketPath = approvalSocketPath(context.record.dataRoot);
122
+ const send = dependencies.send ??
123
+ ((request) => sendAsUser(socketPath, request, context.record.runAs));
124
+ const response = requireOk(await send({ op: 'list' }));
125
+ const pending = 'pending' in response ? response.pending : [];
126
+ if (pending.length === 0) {
127
+ context.write('No devices are waiting for approval.');
128
+ return pending;
129
+ }
130
+ for (const approval of pending) {
131
+ context.write(`${approval.approvalId} ${approval.deviceName} match code ${approval.matchCode}`);
132
+ }
133
+ return pending;
134
+ }
135
+ export async function runResolveApproval(action, approvalId, context, dependencies = {}) {
136
+ const socketPath = approvalSocketPath(context.record.dataRoot);
137
+ const send = dependencies.send ??
138
+ ((request) => sendAsUser(socketPath, request, context.record.runAs));
139
+ const response = requireOk(await send({ op: action, approvalId }));
140
+ const name = 'deviceName' in response ? response.deviceName : approvalId;
141
+ context.write(action === 'approve' ? `${name} is paired.` : `${name} was denied.`);
142
+ }
143
+ //# sourceMappingURL=pairing.js.map