@spexcode/session-selflaunch 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/spex-session.mjs +4 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +156 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +25 -0
- package/dist/locality.d.ts +20 -0
- package/dist/locality.js +83 -0
- package/dist/path.d.ts +13 -0
- package/dist/path.js +45 -0
- package/package.json +35 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SessionProtocol } from '@spexcode/session-protocol';
|
|
2
|
+
import type { SelfLaunchEnvironment } from './path.js';
|
|
3
|
+
type CommandName = 'initialize' | 'enqueue' | 'dequeue' | 'pending';
|
|
4
|
+
interface ParsedCommand {
|
|
5
|
+
readonly command: CommandName;
|
|
6
|
+
readonly values: ReadonlyMap<string, string>;
|
|
7
|
+
readonly headers: readonly [string, string][];
|
|
8
|
+
readonly assumeLocal: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseCommand(argv: readonly string[]): ParsedCommand;
|
|
11
|
+
interface CliDependencies {
|
|
12
|
+
readonly open: (databasePath: string) => SessionProtocol;
|
|
13
|
+
readonly requireLocal: (databasePath: string, options: {
|
|
14
|
+
assumeLocal?: boolean;
|
|
15
|
+
}) => string;
|
|
16
|
+
}
|
|
17
|
+
export interface CliRunOptions {
|
|
18
|
+
readonly argv?: readonly string[];
|
|
19
|
+
readonly env?: SelfLaunchEnvironment;
|
|
20
|
+
readonly stdout?: (text: string) => void;
|
|
21
|
+
readonly stderr?: (text: string) => void;
|
|
22
|
+
readonly dependencies?: CliDependencies;
|
|
23
|
+
}
|
|
24
|
+
export declare function runCli(options?: CliRunOptions): Promise<number>;
|
|
25
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { LocalityError, requireLocalDatabasePath } from './locality.js';
|
|
2
|
+
import { DatabasePathError, resolveDatabasePath } from './path.js';
|
|
3
|
+
class UsageError extends Error {
|
|
4
|
+
code = 'USAGE';
|
|
5
|
+
}
|
|
6
|
+
const VALUE_FLAGS = {
|
|
7
|
+
initialize: new Set(['session-id', 'database-path']),
|
|
8
|
+
enqueue: new Set(['session-id', 'kind', 'body', 'sender-session-id', 'idempotency-key', 'database-path']),
|
|
9
|
+
dequeue: new Set(['session-id', 'database-path']),
|
|
10
|
+
pending: new Set(['session-id', 'database-path']),
|
|
11
|
+
};
|
|
12
|
+
const usage = () => {
|
|
13
|
+
throw new UsageError('usage: spex-session initialize|enqueue|dequeue|pending --session-id ID [command options]');
|
|
14
|
+
};
|
|
15
|
+
const isCommandName = (value) => (value === 'initialize' || value === 'enqueue' || value === 'dequeue' || value === 'pending');
|
|
16
|
+
export function parseCommand(argv) {
|
|
17
|
+
const command = argv[0];
|
|
18
|
+
if (!isCommandName(command))
|
|
19
|
+
return usage();
|
|
20
|
+
const commandName = command;
|
|
21
|
+
const values = new Map();
|
|
22
|
+
const headers = [];
|
|
23
|
+
let assumeLocal = false;
|
|
24
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
25
|
+
const token = argv[index];
|
|
26
|
+
if (token === '--assume-local-storage') {
|
|
27
|
+
if (assumeLocal)
|
|
28
|
+
throw new UsageError('duplicate --assume-local-storage');
|
|
29
|
+
assumeLocal = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!token.startsWith('--'))
|
|
33
|
+
throw new UsageError(`unexpected argument ${token}`);
|
|
34
|
+
const name = token.slice(2);
|
|
35
|
+
if (name === 'header' && commandName === 'enqueue') {
|
|
36
|
+
const raw = argv[++index];
|
|
37
|
+
if (raw === undefined)
|
|
38
|
+
throw new UsageError('missing value for --header');
|
|
39
|
+
const separator = raw.indexOf('=');
|
|
40
|
+
if (separator <= 0)
|
|
41
|
+
throw new UsageError('--header must be K=V');
|
|
42
|
+
headers.push([raw.slice(0, separator), raw.slice(separator + 1)]);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!VALUE_FLAGS[commandName].has(name))
|
|
46
|
+
throw new UsageError(`unknown option --${name}`);
|
|
47
|
+
if (values.has(name))
|
|
48
|
+
throw new UsageError(`duplicate --${name}`);
|
|
49
|
+
const value = argv[++index];
|
|
50
|
+
if (value === undefined)
|
|
51
|
+
throw new UsageError(`missing value for --${name}`);
|
|
52
|
+
values.set(name, value);
|
|
53
|
+
}
|
|
54
|
+
if (!values.has('session-id'))
|
|
55
|
+
throw new UsageError('missing --session-id');
|
|
56
|
+
if (commandName === 'enqueue' && !values.has('kind'))
|
|
57
|
+
throw new UsageError('missing --kind');
|
|
58
|
+
if (commandName === 'enqueue' && !values.has('body'))
|
|
59
|
+
throw new UsageError('missing --body');
|
|
60
|
+
return { command: commandName, values, headers, assumeLocal };
|
|
61
|
+
}
|
|
62
|
+
const renderMessage = (message) => {
|
|
63
|
+
const { body, ...fields } = message;
|
|
64
|
+
return { ...fields, bodyBase64: Buffer.from(body).toString('base64') };
|
|
65
|
+
};
|
|
66
|
+
const errorCode = (error) => {
|
|
67
|
+
if (error instanceof UsageError)
|
|
68
|
+
return error.code;
|
|
69
|
+
if (error instanceof DatabasePathError)
|
|
70
|
+
return error.code;
|
|
71
|
+
if (error instanceof LocalityError)
|
|
72
|
+
return error.code;
|
|
73
|
+
if (typeof error === 'object'
|
|
74
|
+
&& error !== null
|
|
75
|
+
&& 'code' in error
|
|
76
|
+
&& typeof error.code === 'string'
|
|
77
|
+
&& error.code.startsWith('PROTOCOL_'))
|
|
78
|
+
return error.code;
|
|
79
|
+
return 'INTERNAL';
|
|
80
|
+
};
|
|
81
|
+
const SQLITE_EXPERIMENTAL_WARNING = 'SQLite is an experimental feature and might change at any time';
|
|
82
|
+
// @@@node-sqlite-warning - Node 22 emits this during import; suppress only that exact warning so the
|
|
83
|
+
// CLI's stderr remains its frozen single line while every unrelated warning keeps its normal path.
|
|
84
|
+
const loadOpenProtocol = async () => {
|
|
85
|
+
const originalEmitWarning = process.emitWarning;
|
|
86
|
+
process.emitWarning = ((warning, ...args) => {
|
|
87
|
+
const message = warning instanceof Error ? warning.message : warning;
|
|
88
|
+
if (message === SQLITE_EXPERIMENTAL_WARNING)
|
|
89
|
+
return;
|
|
90
|
+
Reflect.apply(originalEmitWarning, process, [warning, ...args]);
|
|
91
|
+
});
|
|
92
|
+
try {
|
|
93
|
+
return (await import('@spexcode/session-protocol')).openProtocol;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
process.emitWarning = originalEmitWarning;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
export async function runCli(options = {}) {
|
|
100
|
+
const stdout = options.stdout ?? (text => process.stdout.write(text));
|
|
101
|
+
const stderr = options.stderr ?? (text => process.stderr.write(text));
|
|
102
|
+
let protocol;
|
|
103
|
+
try {
|
|
104
|
+
const parsed = parseCommand(options.argv ?? process.argv.slice(2));
|
|
105
|
+
const databasePath = resolveDatabasePath({
|
|
106
|
+
databasePath: parsed.values.get('database-path'),
|
|
107
|
+
env: options.env,
|
|
108
|
+
});
|
|
109
|
+
const dependencies = options.dependencies ?? {
|
|
110
|
+
open: await loadOpenProtocol(),
|
|
111
|
+
requireLocal: requireLocalDatabasePath,
|
|
112
|
+
};
|
|
113
|
+
dependencies.requireLocal(databasePath, { assumeLocal: parsed.assumeLocal });
|
|
114
|
+
protocol = dependencies.open(databasePath);
|
|
115
|
+
const sessionId = parsed.values.get('session-id');
|
|
116
|
+
let result;
|
|
117
|
+
if (parsed.command === 'initialize') {
|
|
118
|
+
result = protocol.initialize(sessionId);
|
|
119
|
+
}
|
|
120
|
+
else if (parsed.command === 'enqueue') {
|
|
121
|
+
const headers = Object.fromEntries(parsed.headers);
|
|
122
|
+
result = renderMessage(protocol.enqueue(sessionId, {
|
|
123
|
+
kind: parsed.values.get('kind'),
|
|
124
|
+
body: Buffer.from(parsed.values.get('body'), 'utf8'),
|
|
125
|
+
...(parsed.values.has('sender-session-id')
|
|
126
|
+
? { senderSessionId: parsed.values.get('sender-session-id') }
|
|
127
|
+
: {}),
|
|
128
|
+
...(parsed.values.has('idempotency-key')
|
|
129
|
+
? { idempotencyKey: parsed.values.get('idempotency-key') }
|
|
130
|
+
: {}),
|
|
131
|
+
...(parsed.headers.length > 0 ? { headers } : {}),
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
else if (parsed.command === 'dequeue') {
|
|
135
|
+
const message = protocol.dequeue(sessionId);
|
|
136
|
+
result = message === null ? null : renderMessage(message);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
result = protocol.listPending(sessionId).map(renderMessage);
|
|
140
|
+
}
|
|
141
|
+
stdout(`${JSON.stringify(result)}\n`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const code = errorCode(error);
|
|
146
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
147
|
+
const repair = code === 'PROTOCOL_PATH_PARENT_MISSING'
|
|
148
|
+
? '; create the parent directory or choose --database-path with an existing parent'
|
|
149
|
+
: '';
|
|
150
|
+
stderr(`spex-session: ${code}: ${message}${repair}\n`);
|
|
151
|
+
return code === 'USAGE' ? 2 : 1;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
protocol?.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export { LocalityError, requireLocalDatabasePath } from './locality.js';
|
|
2
|
+
export type { LocalityRefusalCode } from './locality.js';
|
|
3
|
+
export { resolveDatabasePath } from './path.js';
|
|
4
|
+
export type { ResolveDatabasePathOptions, SelfLaunchEnvironment } from './path.js';
|
|
5
|
+
import type { ProtocolTransaction } from '@spexcode/session-protocol';
|
|
6
|
+
import { type BindingOptions, type RuntimeBinding } from '@spexcode/session-runtime';
|
|
7
|
+
/**
|
|
8
|
+
* Native identity supplied by the harness adapter that owns this self-launch.
|
|
9
|
+
* The CLI session id is deliberately not accepted as a native identity.
|
|
10
|
+
*/
|
|
11
|
+
export interface SelfLaunchRuntimeIdentity {
|
|
12
|
+
nativeSessionId: string;
|
|
13
|
+
nativeStartToken: string;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
export interface SelfLaunchProtocol {
|
|
17
|
+
withTransaction<T>(body: (tx: ProtocolTransaction) => T): T;
|
|
18
|
+
}
|
|
19
|
+
export interface SelfLaunchBindingOptions extends BindingOptions {
|
|
20
|
+
runtimeKind?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Attach a caller-owned native harness identity to an existing protocol address.
|
|
24
|
+
* This is the only self-launch/runtime seam; it does not launch, probe, or stop a harness.
|
|
25
|
+
*/
|
|
26
|
+
export declare function bindSelfLaunchRuntime(protocol: SelfLaunchProtocol, protocolSessionId: string, identity: SelfLaunchRuntimeIdentity, options?: SelfLaunchBindingOptions): RuntimeBinding;
|
|
27
|
+
export declare function resolveSelfLaunchRuntime(protocol: SelfLaunchProtocol, protocolSessionId: string): RuntimeBinding | null;
|
|
28
|
+
export declare function unbindSelfLaunchRuntime(protocol: SelfLaunchProtocol, protocolSessionId: string, options?: BindingOptions): RuntimeBinding;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export { LocalityError, requireLocalDatabasePath } from './locality.js';
|
|
2
|
+
export { resolveDatabasePath } from './path.js';
|
|
3
|
+
import { openRuntimeBindings, } from '@spexcode/session-runtime';
|
|
4
|
+
const SELF_LAUNCH_NAMESPACE = 'self-launch';
|
|
5
|
+
/**
|
|
6
|
+
* Attach a caller-owned native harness identity to an existing protocol address.
|
|
7
|
+
* This is the only self-launch/runtime seam; it does not launch, probe, or stop a harness.
|
|
8
|
+
*/
|
|
9
|
+
export function bindSelfLaunchRuntime(protocol, protocolSessionId, identity, options = {}) {
|
|
10
|
+
const bindings = openRuntimeBindings(protocol);
|
|
11
|
+
return protocol.withTransaction(tx => bindings.bind(tx, protocolSessionId, {
|
|
12
|
+
namespace: SELF_LAUNCH_NAMESPACE,
|
|
13
|
+
runtimeKind: options.runtimeKind ?? 'self-launch',
|
|
14
|
+
nativeSessionId: identity.nativeSessionId,
|
|
15
|
+
nativeStartToken: identity.nativeStartToken,
|
|
16
|
+
metadata: identity.metadata,
|
|
17
|
+
}, options));
|
|
18
|
+
}
|
|
19
|
+
export function resolveSelfLaunchRuntime(protocol, protocolSessionId) {
|
|
20
|
+
return openRuntimeBindings(protocol).resolve(SELF_LAUNCH_NAMESPACE, protocolSessionId);
|
|
21
|
+
}
|
|
22
|
+
export function unbindSelfLaunchRuntime(protocol, protocolSessionId, options = {}) {
|
|
23
|
+
const bindings = openRuntimeBindings(protocol);
|
|
24
|
+
return protocol.withTransaction(tx => bindings.unbind(tx, SELF_LAUNCH_NAMESPACE, protocolSessionId, options));
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type LocalityRefusalCode = 'LOCALITY_NETWORK_FILESYSTEM' | 'LOCALITY_UNDETERMINED' | 'LOCALITY_DETECTOR_UNAVAILABLE' | 'LOCALITY_PROBE_FAILED';
|
|
2
|
+
export declare class LocalityError extends Error {
|
|
3
|
+
readonly code: LocalityRefusalCode;
|
|
4
|
+
constructor(code: LocalityRefusalCode, message: string, cause?: unknown);
|
|
5
|
+
}
|
|
6
|
+
export interface FilesystemClassification {
|
|
7
|
+
readonly locality: 'local' | 'network' | 'undetermined';
|
|
8
|
+
readonly name: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function classifyFilesystemType(type: number | bigint): FilesystemClassification;
|
|
11
|
+
export interface LocalityDetector {
|
|
12
|
+
readonly platform: string;
|
|
13
|
+
statfsType(parentPath: string): number | bigint;
|
|
14
|
+
}
|
|
15
|
+
export declare function requireLocalDatabasePathWithDetector(databasePath: string, options: {
|
|
16
|
+
assumeLocal?: boolean;
|
|
17
|
+
}, detector: LocalityDetector): string;
|
|
18
|
+
export declare function requireLocalDatabasePath(databasePath: string, options?: {
|
|
19
|
+
assumeLocal?: boolean;
|
|
20
|
+
}): string;
|
package/dist/locality.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { statfsSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, dirname } from 'node:path';
|
|
3
|
+
import { DatabasePathError } from './path.js';
|
|
4
|
+
export class LocalityError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
constructor(code, message, cause) {
|
|
7
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
8
|
+
this.name = 'LocalityError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
// @@@network-magic-evidence - These header values drive classifier vectors, not real-mount evidence;
|
|
13
|
+
// this host has no corresponding network mount on which to run them.
|
|
14
|
+
const NETWORK_FILESYSTEM_TYPES = [
|
|
15
|
+
{ name: 'NFS', type: 0x6969 },
|
|
16
|
+
{ name: 'SMB', type: 0x517b },
|
|
17
|
+
{ name: 'CIFS', type: 0xff534d42 },
|
|
18
|
+
{ name: 'SMB2', type: 0xfe534d42 },
|
|
19
|
+
{ name: '9P', type: 0x01021997 },
|
|
20
|
+
{ name: 'CEPH', type: 0x00c36400 },
|
|
21
|
+
{ name: 'AFS', type: 0x5346414f },
|
|
22
|
+
{ name: 'AFS_FS', type: 0x6b414653 },
|
|
23
|
+
{ name: 'CODA', type: 0x73757245 },
|
|
24
|
+
{ name: 'OCFS2', type: 0x7461636f },
|
|
25
|
+
{ name: 'NCP', type: 0x564c },
|
|
26
|
+
];
|
|
27
|
+
const LOCAL_FILESYSTEM_TYPES = [
|
|
28
|
+
{ name: 'EXT2/3/4', type: 0xef53 },
|
|
29
|
+
{ name: 'BTRFS', type: 0x9123683e },
|
|
30
|
+
{ name: 'XFS', type: 0x58465342 },
|
|
31
|
+
{ name: 'F2FS', type: 0xf2f52010 },
|
|
32
|
+
{ name: 'TMPFS', type: 0x01021994 },
|
|
33
|
+
{ name: 'OVERLAYFS', type: 0x794c7630 },
|
|
34
|
+
{ name: 'ZFS', type: 0x2fc12fc1 },
|
|
35
|
+
];
|
|
36
|
+
const unsignedMagic = (type) => Number(BigInt.asUintN(32, BigInt(type)));
|
|
37
|
+
export function classifyFilesystemType(type) {
|
|
38
|
+
const magic = unsignedMagic(type);
|
|
39
|
+
const network = NETWORK_FILESYSTEM_TYPES.find(candidate => candidate.type === magic);
|
|
40
|
+
if (network)
|
|
41
|
+
return { locality: 'network', name: network.name };
|
|
42
|
+
const local = LOCAL_FILESYSTEM_TYPES.find(candidate => candidate.type === magic);
|
|
43
|
+
if (local)
|
|
44
|
+
return { locality: 'local', name: local.name };
|
|
45
|
+
return { locality: 'undetermined', name: `0x${magic.toString(16)}` };
|
|
46
|
+
}
|
|
47
|
+
export function requireLocalDatabasePathWithDetector(databasePath, options, detector) {
|
|
48
|
+
if (!isAbsolute(databasePath)) {
|
|
49
|
+
throw new DatabasePathError('PROTOCOL_PATH_NOT_ABSOLUTE', 'databasePath must be absolute before locality detection');
|
|
50
|
+
}
|
|
51
|
+
if (options.assumeLocal)
|
|
52
|
+
return databasePath;
|
|
53
|
+
const parent = dirname(databasePath);
|
|
54
|
+
if (detector.platform !== 'linux') {
|
|
55
|
+
throw new LocalityError('LOCALITY_DETECTOR_UNAVAILABLE', `no filesystem locality detector for platform ${detector.platform}; pass --assume-local-storage only after auditing ${parent}`);
|
|
56
|
+
}
|
|
57
|
+
let type;
|
|
58
|
+
try {
|
|
59
|
+
type = detector.statfsType(parent);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
// @@@missing-parent - Preserve the actionable path error without pretending locality was established.
|
|
63
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT') {
|
|
64
|
+
throw new DatabasePathError('PROTOCOL_PATH_PARENT_MISSING', `database parent directory does not exist: ${parent}`, error);
|
|
65
|
+
}
|
|
66
|
+
throw new LocalityError('LOCALITY_PROBE_FAILED', `could not determine the filesystem of ${parent}`, error);
|
|
67
|
+
}
|
|
68
|
+
const classification = classifyFilesystemType(type);
|
|
69
|
+
if (classification.locality === 'network') {
|
|
70
|
+
throw new LocalityError('LOCALITY_NETWORK_FILESYSTEM', `${parent} is ${classification.name}; advisory locking is not admitted there`);
|
|
71
|
+
}
|
|
72
|
+
if (classification.locality === 'undetermined') {
|
|
73
|
+
throw new LocalityError('LOCALITY_UNDETERMINED', `filesystem type ${classification.name} at ${parent} is not on the audited local allow-list`);
|
|
74
|
+
}
|
|
75
|
+
return databasePath;
|
|
76
|
+
}
|
|
77
|
+
const linuxDetector = {
|
|
78
|
+
platform: process.platform,
|
|
79
|
+
statfsType: parentPath => statfsSync(parentPath).type,
|
|
80
|
+
};
|
|
81
|
+
export function requireLocalDatabasePath(databasePath, options = {}) {
|
|
82
|
+
return requireLocalDatabasePathWithDetector(databasePath, options, linuxDetector);
|
|
83
|
+
}
|
package/dist/path.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type DatabasePathErrorCode = 'PROTOCOL_PATH_NOT_ABSOLUTE' | 'PROTOCOL_PATH_INVALID' | 'PROTOCOL_PATH_PARENT_MISSING';
|
|
2
|
+
export declare class DatabasePathError extends Error {
|
|
3
|
+
readonly code: DatabasePathErrorCode;
|
|
4
|
+
constructor(code: DatabasePathErrorCode, message: string, cause?: unknown);
|
|
5
|
+
}
|
|
6
|
+
export type SelfLaunchEnvironment = Readonly<Record<string, string | undefined>>;
|
|
7
|
+
export interface ResolveDatabasePathOptions {
|
|
8
|
+
databasePath?: string;
|
|
9
|
+
env?: SelfLaunchEnvironment;
|
|
10
|
+
readFile?: (path: string) => string;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveDatabasePath(options?: ResolveDatabasePathOptions): string;
|
|
13
|
+
export {};
|
package/dist/path.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
export class DatabasePathError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message, cause) {
|
|
6
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
7
|
+
this.name = 'DatabasePathError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function resolveDatabasePath(options = {}) {
|
|
12
|
+
const env = options.env ?? process.env;
|
|
13
|
+
let databasePath = options.databasePath ?? env.SPEX_SESSION_DATABASE_PATH;
|
|
14
|
+
if (databasePath === undefined && env.SPEX_SESSION_CONFIG) {
|
|
15
|
+
const configPath = env.SPEX_SESSION_CONFIG;
|
|
16
|
+
let config;
|
|
17
|
+
try {
|
|
18
|
+
config = JSON.parse((options.readFile ?? ((path) => readFileSync(path, 'utf8')))(configPath));
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new DatabasePathError('PROTOCOL_PATH_INVALID', `could not read session config ${configPath}`, error);
|
|
22
|
+
}
|
|
23
|
+
if (typeof config !== 'object'
|
|
24
|
+
|| config === null
|
|
25
|
+
|| typeof config.databasePath !== 'string'
|
|
26
|
+
|| !config.databasePath) {
|
|
27
|
+
throw new DatabasePathError('PROTOCOL_PATH_INVALID', `session config ${configPath} must contain databasePath`);
|
|
28
|
+
}
|
|
29
|
+
databasePath = config.databasePath;
|
|
30
|
+
}
|
|
31
|
+
if (databasePath === undefined) {
|
|
32
|
+
const home = env.SPEXCODE_HOME || (env.HOME ? join(env.HOME, '.spexcode') : undefined);
|
|
33
|
+
if (!home) {
|
|
34
|
+
throw new DatabasePathError('PROTOCOL_PATH_INVALID', 'HOME or SPEXCODE_HOME is required for the default database path');
|
|
35
|
+
}
|
|
36
|
+
databasePath = join(home, 'sessions.sqlite');
|
|
37
|
+
}
|
|
38
|
+
if (databasePath.length === 0) {
|
|
39
|
+
throw new DatabasePathError('PROTOCOL_PATH_INVALID', 'databasePath must not be empty');
|
|
40
|
+
}
|
|
41
|
+
if (!isAbsolute(databasePath)) {
|
|
42
|
+
throw new DatabasePathError('PROTOCOL_PATH_NOT_ABSOLUTE', 'databasePath must be absolute and is never resolved from cwd');
|
|
43
|
+
}
|
|
44
|
+
return databasePath;
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@spexcode/session-selflaunch",
|
|
3
|
+
"version": "0.6.8",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "A fail-closed self-launch adopter for the SpexCode session protocol.",
|
|
7
|
+
"bin": {
|
|
8
|
+
"spex-session": "./bin/spex-session.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=22"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "node ../../scripts/build-dist.mjs",
|
|
23
|
+
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
24
|
+
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@spexcode/session-protocol": "0.6.8",
|
|
28
|
+
"@spexcode/session-runtime": "0.6.8"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.16.0",
|
|
32
|
+
"tsx": "^4.19.2",
|
|
33
|
+
"typescript": "^5.6.3"
|
|
34
|
+
}
|
|
35
|
+
}
|