@superdoc/sdk 2.19.0-next.9 → 2.19.1-next.1
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/README.md +36 -0
- package/dist/generated/client.d.ts +2 -0
- package/dist/generated/contract.cjs +1 -1
- package/dist/generated/contract.js +1 -1
- package/dist/runtime/host.cjs +61 -19
- package/dist/runtime/host.d.ts +3 -0
- package/dist/runtime/host.js +61 -19
- package/dist/runtime/sdk-version.generated.cjs +1 -1
- package/dist/runtime/sdk-version.generated.d.ts +1 -1
- package/dist/runtime/sdk-version.generated.js +1 -1
- package/dist/runtime/state-directory.cjs +132 -0
- package/dist/runtime/state-directory.d.ts +14 -0
- package/dist/runtime/state-directory.js +127 -0
- package/dist/runtime/transport-common.d.ts +2 -0
- package/package.json +7 -7
- package/tools/tools-policy.json +1 -1
package/dist/runtime/host.cjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var node_child_process = require('node:child_process');
|
|
4
4
|
var transportCommon = require('./transport-common.cjs');
|
|
5
5
|
var errors = require('./errors.cjs');
|
|
6
|
+
var stateDirectory = require('./state-directory.cjs');
|
|
6
7
|
var documentEvidence = require('./document-evidence.cjs');
|
|
7
8
|
var contract = require('../generated/contract.cjs');
|
|
8
9
|
var documentRpc = require('./document-rpc.cjs');
|
|
@@ -84,6 +85,8 @@ function resolveJsWatchdogTimeout(watchdogTimeoutMs, requestTimeoutMs, timeoutMs
|
|
|
84
85
|
}
|
|
85
86
|
/** Transport for the legacy CLI host or the structured document host. */
|
|
86
87
|
class HostTransport {
|
|
88
|
+
stateDirectory;
|
|
89
|
+
stoppedChildren = new Map();
|
|
87
90
|
hostBin;
|
|
88
91
|
processMode;
|
|
89
92
|
env;
|
|
@@ -112,6 +115,7 @@ class HostTransport {
|
|
|
112
115
|
documentRpcMutationStates = new Map();
|
|
113
116
|
indeterminateDiscardCloses = new Set();
|
|
114
117
|
constructor(options) {
|
|
118
|
+
this.stateDirectory = new stateDirectory.SdkStateDirectory(options.stateDir);
|
|
115
119
|
this.hostBin = options.hostBin;
|
|
116
120
|
this.processMode = options.processMode ?? 'cli';
|
|
117
121
|
this.env = options.env;
|
|
@@ -175,7 +179,11 @@ class HostTransport {
|
|
|
175
179
|
return this.disposePromise;
|
|
176
180
|
if (this.terminalDisposalError)
|
|
177
181
|
throw this.terminalDisposalError;
|
|
178
|
-
if (!this.child &&
|
|
182
|
+
if (!this.child &&
|
|
183
|
+
!this.connecting &&
|
|
184
|
+
this.activeCalls.size === 0 &&
|
|
185
|
+
!this.stateDirectory.ownsRoot &&
|
|
186
|
+
this.stoppedChildren.size === 0)
|
|
179
187
|
return;
|
|
180
188
|
this.stopping = true;
|
|
181
189
|
const disposal = this.disposeAfterActiveCalls();
|
|
@@ -193,8 +201,11 @@ class HostTransport {
|
|
|
193
201
|
await Promise.allSettled(Array.from(this.activeCalls));
|
|
194
202
|
if (this.terminalDisposalError)
|
|
195
203
|
throw this.terminalDisposalError;
|
|
196
|
-
if (!this.child)
|
|
204
|
+
if (!this.child) {
|
|
205
|
+
await this.waitForStoppedChildren();
|
|
206
|
+
await this.stateDirectory.dispose();
|
|
197
207
|
return;
|
|
208
|
+
}
|
|
198
209
|
try {
|
|
199
210
|
await this.reconcileIndeterminateDiscardCloses();
|
|
200
211
|
await this.persistDirtyDocumentRpcSessions();
|
|
@@ -212,17 +223,16 @@ class HostTransport {
|
|
|
212
223
|
catch {
|
|
213
224
|
// ignore and force shutdown below
|
|
214
225
|
}
|
|
215
|
-
await
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
child.once('close', () => {
|
|
221
|
-
clearTimeout(timer);
|
|
222
|
-
resolve();
|
|
226
|
+
await this.waitForStoppedChildren(true);
|
|
227
|
+
if (child.exitCode !== 0) {
|
|
228
|
+
this.terminalDisposalError = new errors.SuperDocCliError('Host shutdown failed; its state directory has been retained.', {
|
|
229
|
+
code: 'HOST_SHUTDOWN_FAILED',
|
|
230
|
+
details: { path: this.stateDirectory.path, exitCode: child.exitCode, signal: child.signalCode },
|
|
223
231
|
});
|
|
224
|
-
|
|
232
|
+
throw this.terminalDisposalError;
|
|
233
|
+
}
|
|
225
234
|
this.cleanupProcess(null);
|
|
235
|
+
await this.stateDirectory.dispose();
|
|
226
236
|
}
|
|
227
237
|
async invoke(operation, params = {}, options = {}) {
|
|
228
238
|
return this.runWhileActive(() => this.invokeWhileActive(operation, params, options));
|
|
@@ -452,14 +462,15 @@ class HostTransport {
|
|
|
452
462
|
const args = this.processMode === 'document'
|
|
453
463
|
? buildDocumentHostSpawnArgs(prefixArgs)
|
|
454
464
|
: buildHostSpawnArgs(prefixArgs, { requestTimeoutMs: this.requestTimeoutMs });
|
|
465
|
+
await this.waitForStoppedChildren();
|
|
466
|
+
const env = { ...process.env, ...this.env };
|
|
467
|
+
env.SUPERDOC_CLI_STATE_DIR = await this.stateDirectory.prepare(env);
|
|
455
468
|
const child = node_child_process.spawn(command, args, {
|
|
456
|
-
env
|
|
457
|
-
...process.env,
|
|
458
|
-
...this.env,
|
|
459
|
-
},
|
|
469
|
+
env,
|
|
460
470
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
461
471
|
});
|
|
462
472
|
this.child = child;
|
|
473
|
+
this.stoppedChildren.set(child, new Promise((resolve) => child.once('close', () => resolve())));
|
|
463
474
|
// JSON-RPC is framed by ASCII LF; Node 24 readline also splits on a raw
|
|
464
475
|
// U+2028 inside otherwise valid JSON string data.
|
|
465
476
|
child.stdout.setEncoding('utf8');
|
|
@@ -473,6 +484,8 @@ class HostTransport {
|
|
|
473
484
|
process.stderr.write(`[superdoc-host] ${String(chunk)}`);
|
|
474
485
|
});
|
|
475
486
|
child.on('error', (error) => {
|
|
487
|
+
if (this.child !== child)
|
|
488
|
+
return;
|
|
476
489
|
this.handleDisconnect(new errors.SuperDocCliError('Host process failed.', {
|
|
477
490
|
code: 'HOST_DISCONNECTED',
|
|
478
491
|
details: {
|
|
@@ -481,6 +494,8 @@ class HostTransport {
|
|
|
481
494
|
}));
|
|
482
495
|
});
|
|
483
496
|
child.on('close', (code, signal) => {
|
|
497
|
+
if (this.child !== child)
|
|
498
|
+
return;
|
|
484
499
|
if (this.stopping && this.dirtyDocumentRpcSessions.size === 0) {
|
|
485
500
|
this.cleanupProcess(null);
|
|
486
501
|
return;
|
|
@@ -738,11 +753,11 @@ class HostTransport {
|
|
|
738
753
|
const cliMessage = typeof data?.message === 'string' ? data.message : undefined;
|
|
739
754
|
const exitCode = typeof data?.exitCode === 'number' ? data.exitCode : undefined;
|
|
740
755
|
if (cliCode) {
|
|
741
|
-
return new errors.SuperDocCliError(cliMessage ?? error.message ?? 'Command failed.', {
|
|
756
|
+
return this.stateDirectory.mapOperationError(new errors.SuperDocCliError(cliMessage ?? error.message ?? 'Command failed.', {
|
|
742
757
|
code: cliCode,
|
|
743
758
|
details: data?.details,
|
|
744
759
|
exitCode,
|
|
745
|
-
});
|
|
760
|
+
}));
|
|
746
761
|
}
|
|
747
762
|
if (domainCode) {
|
|
748
763
|
return new errors.SuperDocCliError(error.message, {
|
|
@@ -765,6 +780,33 @@ class HostTransport {
|
|
|
765
780
|
details: data,
|
|
766
781
|
});
|
|
767
782
|
}
|
|
783
|
+
async waitForStoppedChildren(forceAfterTimeout = false) {
|
|
784
|
+
for (const [child, stopped] of this.stoppedChildren) {
|
|
785
|
+
let timer;
|
|
786
|
+
try {
|
|
787
|
+
await new Promise((resolve, reject) => {
|
|
788
|
+
const fail = () => reject(new errors.SuperDocCliError('Host process did not stop; state has been retained.', {
|
|
789
|
+
code: 'HOST_DISCONNECTED',
|
|
790
|
+
details: { pid: child.pid },
|
|
791
|
+
}));
|
|
792
|
+
timer = setTimeout(() => {
|
|
793
|
+
if (!forceAfterTimeout) {
|
|
794
|
+
fail();
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
child.kill('SIGKILL');
|
|
798
|
+
timer = setTimeout(fail, this.shutdownTimeoutMs);
|
|
799
|
+
}, this.shutdownTimeoutMs);
|
|
800
|
+
void stopped.then(resolve, reject);
|
|
801
|
+
});
|
|
802
|
+
this.stoppedChildren.delete(child);
|
|
803
|
+
}
|
|
804
|
+
finally {
|
|
805
|
+
if (timer)
|
|
806
|
+
clearTimeout(timer);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
768
810
|
handleDisconnect(error) {
|
|
769
811
|
this.cleanupProcess(error);
|
|
770
812
|
}
|
|
@@ -784,8 +826,8 @@ class HostTransport {
|
|
|
784
826
|
if (child) {
|
|
785
827
|
if (this.stdoutDataListener)
|
|
786
828
|
child.stdout.off('data', this.stdoutDataListener);
|
|
787
|
-
child.
|
|
788
|
-
|
|
829
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
830
|
+
child.kill('SIGKILL');
|
|
789
831
|
}
|
|
790
832
|
this.child = null;
|
|
791
833
|
this.stdoutBuffer = '';
|
package/dist/runtime/host.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export declare function buildDocumentHostSpawnArgs(prefixArgs: readonly string[]
|
|
|
31
31
|
export declare function resolveJsWatchdogTimeout(watchdogTimeoutMs: number, requestTimeoutMs: number | undefined, timeoutMsOverride: number | undefined): number;
|
|
32
32
|
/** Transport for the legacy CLI host or the structured document host. */
|
|
33
33
|
export declare class HostTransport {
|
|
34
|
+
private readonly stateDirectory;
|
|
35
|
+
private readonly stoppedChildren;
|
|
34
36
|
private readonly hostBin;
|
|
35
37
|
private readonly processMode;
|
|
36
38
|
private readonly env?;
|
|
@@ -87,6 +89,7 @@ export declare class HostTransport {
|
|
|
87
89
|
private onStdoutLine;
|
|
88
90
|
private onStdoutChunk;
|
|
89
91
|
private mapJsonRpcError;
|
|
92
|
+
private waitForStoppedChildren;
|
|
90
93
|
private handleDisconnect;
|
|
91
94
|
private cleanupProcess;
|
|
92
95
|
}
|
package/dist/runtime/host.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { buildOperationArgv, buildStructuredReadInput, normalizeCollaborationAuth, resolveInvocation, } from './transport-common.js';
|
|
3
3
|
import { SuperDocCliError } from './errors.js';
|
|
4
|
+
import { SdkStateDirectory } from './state-directory.js';
|
|
4
5
|
import { registerMutationRevision } from './document-evidence.js';
|
|
5
6
|
import { CONTRACT } from '../generated/contract.js';
|
|
6
7
|
import { DOCUMENT_RPC_FEATURES, DOCUMENT_RPC_DEFAULT_CHANGE_MODE_FEATURE, DOCUMENT_RPC_REQUEST_TIMEOUT_FEATURE, DOCUMENT_RPC_REPLACE_FILE_FEATURE, DOCUMENT_RPC_SOURCE_SAVE_FEATURE, buildDocumentInvokeParams, buildDocumentOpenParams, canOpenWithDocumentRpc, documentRpcRequestSupportsResponseTimeout, documentRpcEnabled, mapDocumentLifecycleResult, mapDocumentOpenResult, mapDocumentReplaceFileResult, supportsDocumentRpc, } from './document-rpc.js';
|
|
@@ -81,6 +82,8 @@ export function resolveJsWatchdogTimeout(watchdogTimeoutMs, requestTimeoutMs, ti
|
|
|
81
82
|
}
|
|
82
83
|
/** Transport for the legacy CLI host or the structured document host. */
|
|
83
84
|
export class HostTransport {
|
|
85
|
+
stateDirectory;
|
|
86
|
+
stoppedChildren = new Map();
|
|
84
87
|
hostBin;
|
|
85
88
|
processMode;
|
|
86
89
|
env;
|
|
@@ -109,6 +112,7 @@ export class HostTransport {
|
|
|
109
112
|
documentRpcMutationStates = new Map();
|
|
110
113
|
indeterminateDiscardCloses = new Set();
|
|
111
114
|
constructor(options) {
|
|
115
|
+
this.stateDirectory = new SdkStateDirectory(options.stateDir);
|
|
112
116
|
this.hostBin = options.hostBin;
|
|
113
117
|
this.processMode = options.processMode ?? 'cli';
|
|
114
118
|
this.env = options.env;
|
|
@@ -172,7 +176,11 @@ export class HostTransport {
|
|
|
172
176
|
return this.disposePromise;
|
|
173
177
|
if (this.terminalDisposalError)
|
|
174
178
|
throw this.terminalDisposalError;
|
|
175
|
-
if (!this.child &&
|
|
179
|
+
if (!this.child &&
|
|
180
|
+
!this.connecting &&
|
|
181
|
+
this.activeCalls.size === 0 &&
|
|
182
|
+
!this.stateDirectory.ownsRoot &&
|
|
183
|
+
this.stoppedChildren.size === 0)
|
|
176
184
|
return;
|
|
177
185
|
this.stopping = true;
|
|
178
186
|
const disposal = this.disposeAfterActiveCalls();
|
|
@@ -190,8 +198,11 @@ export class HostTransport {
|
|
|
190
198
|
await Promise.allSettled(Array.from(this.activeCalls));
|
|
191
199
|
if (this.terminalDisposalError)
|
|
192
200
|
throw this.terminalDisposalError;
|
|
193
|
-
if (!this.child)
|
|
201
|
+
if (!this.child) {
|
|
202
|
+
await this.waitForStoppedChildren();
|
|
203
|
+
await this.stateDirectory.dispose();
|
|
194
204
|
return;
|
|
205
|
+
}
|
|
195
206
|
try {
|
|
196
207
|
await this.reconcileIndeterminateDiscardCloses();
|
|
197
208
|
await this.persistDirtyDocumentRpcSessions();
|
|
@@ -209,17 +220,16 @@ export class HostTransport {
|
|
|
209
220
|
catch {
|
|
210
221
|
// ignore and force shutdown below
|
|
211
222
|
}
|
|
212
|
-
await
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
child.once('close', () => {
|
|
218
|
-
clearTimeout(timer);
|
|
219
|
-
resolve();
|
|
223
|
+
await this.waitForStoppedChildren(true);
|
|
224
|
+
if (child.exitCode !== 0) {
|
|
225
|
+
this.terminalDisposalError = new SuperDocCliError('Host shutdown failed; its state directory has been retained.', {
|
|
226
|
+
code: 'HOST_SHUTDOWN_FAILED',
|
|
227
|
+
details: { path: this.stateDirectory.path, exitCode: child.exitCode, signal: child.signalCode },
|
|
220
228
|
});
|
|
221
|
-
|
|
229
|
+
throw this.terminalDisposalError;
|
|
230
|
+
}
|
|
222
231
|
this.cleanupProcess(null);
|
|
232
|
+
await this.stateDirectory.dispose();
|
|
223
233
|
}
|
|
224
234
|
async invoke(operation, params = {}, options = {}) {
|
|
225
235
|
return this.runWhileActive(() => this.invokeWhileActive(operation, params, options));
|
|
@@ -449,14 +459,15 @@ export class HostTransport {
|
|
|
449
459
|
const args = this.processMode === 'document'
|
|
450
460
|
? buildDocumentHostSpawnArgs(prefixArgs)
|
|
451
461
|
: buildHostSpawnArgs(prefixArgs, { requestTimeoutMs: this.requestTimeoutMs });
|
|
462
|
+
await this.waitForStoppedChildren();
|
|
463
|
+
const env = { ...process.env, ...this.env };
|
|
464
|
+
env.SUPERDOC_CLI_STATE_DIR = await this.stateDirectory.prepare(env);
|
|
452
465
|
const child = spawn(command, args, {
|
|
453
|
-
env
|
|
454
|
-
...process.env,
|
|
455
|
-
...this.env,
|
|
456
|
-
},
|
|
466
|
+
env,
|
|
457
467
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
458
468
|
});
|
|
459
469
|
this.child = child;
|
|
470
|
+
this.stoppedChildren.set(child, new Promise((resolve) => child.once('close', () => resolve())));
|
|
460
471
|
// JSON-RPC is framed by ASCII LF; Node 24 readline also splits on a raw
|
|
461
472
|
// U+2028 inside otherwise valid JSON string data.
|
|
462
473
|
child.stdout.setEncoding('utf8');
|
|
@@ -470,6 +481,8 @@ export class HostTransport {
|
|
|
470
481
|
process.stderr.write(`[superdoc-host] ${String(chunk)}`);
|
|
471
482
|
});
|
|
472
483
|
child.on('error', (error) => {
|
|
484
|
+
if (this.child !== child)
|
|
485
|
+
return;
|
|
473
486
|
this.handleDisconnect(new SuperDocCliError('Host process failed.', {
|
|
474
487
|
code: 'HOST_DISCONNECTED',
|
|
475
488
|
details: {
|
|
@@ -478,6 +491,8 @@ export class HostTransport {
|
|
|
478
491
|
}));
|
|
479
492
|
});
|
|
480
493
|
child.on('close', (code, signal) => {
|
|
494
|
+
if (this.child !== child)
|
|
495
|
+
return;
|
|
481
496
|
if (this.stopping && this.dirtyDocumentRpcSessions.size === 0) {
|
|
482
497
|
this.cleanupProcess(null);
|
|
483
498
|
return;
|
|
@@ -735,11 +750,11 @@ export class HostTransport {
|
|
|
735
750
|
const cliMessage = typeof data?.message === 'string' ? data.message : undefined;
|
|
736
751
|
const exitCode = typeof data?.exitCode === 'number' ? data.exitCode : undefined;
|
|
737
752
|
if (cliCode) {
|
|
738
|
-
return new SuperDocCliError(cliMessage ?? error.message ?? 'Command failed.', {
|
|
753
|
+
return this.stateDirectory.mapOperationError(new SuperDocCliError(cliMessage ?? error.message ?? 'Command failed.', {
|
|
739
754
|
code: cliCode,
|
|
740
755
|
details: data?.details,
|
|
741
756
|
exitCode,
|
|
742
|
-
});
|
|
757
|
+
}));
|
|
743
758
|
}
|
|
744
759
|
if (domainCode) {
|
|
745
760
|
return new SuperDocCliError(error.message, {
|
|
@@ -762,6 +777,33 @@ export class HostTransport {
|
|
|
762
777
|
details: data,
|
|
763
778
|
});
|
|
764
779
|
}
|
|
780
|
+
async waitForStoppedChildren(forceAfterTimeout = false) {
|
|
781
|
+
for (const [child, stopped] of this.stoppedChildren) {
|
|
782
|
+
let timer;
|
|
783
|
+
try {
|
|
784
|
+
await new Promise((resolve, reject) => {
|
|
785
|
+
const fail = () => reject(new SuperDocCliError('Host process did not stop; state has been retained.', {
|
|
786
|
+
code: 'HOST_DISCONNECTED',
|
|
787
|
+
details: { pid: child.pid },
|
|
788
|
+
}));
|
|
789
|
+
timer = setTimeout(() => {
|
|
790
|
+
if (!forceAfterTimeout) {
|
|
791
|
+
fail();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
child.kill('SIGKILL');
|
|
795
|
+
timer = setTimeout(fail, this.shutdownTimeoutMs);
|
|
796
|
+
}, this.shutdownTimeoutMs);
|
|
797
|
+
void stopped.then(resolve, reject);
|
|
798
|
+
});
|
|
799
|
+
this.stoppedChildren.delete(child);
|
|
800
|
+
}
|
|
801
|
+
finally {
|
|
802
|
+
if (timer)
|
|
803
|
+
clearTimeout(timer);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
765
807
|
handleDisconnect(error) {
|
|
766
808
|
this.cleanupProcess(error);
|
|
767
809
|
}
|
|
@@ -781,8 +823,8 @@ export class HostTransport {
|
|
|
781
823
|
if (child) {
|
|
782
824
|
if (this.stdoutDataListener)
|
|
783
825
|
child.stdout.off('data', this.stdoutDataListener);
|
|
784
|
-
child.
|
|
785
|
-
|
|
826
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
827
|
+
child.kill('SIGKILL');
|
|
786
828
|
}
|
|
787
829
|
this.child = null;
|
|
788
830
|
this.stdoutBuffer = '';
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
// AUTO-GENERATED by scripts/embed-version.mjs — DO NOT EDIT.
|
|
4
4
|
// Source of truth: package.json. Regenerated on every SDK build so the
|
|
5
5
|
// SDK retains its own version identity when bundled into another package.
|
|
6
|
-
const SDK_VERSION = '2.19.
|
|
6
|
+
const SDK_VERSION = '2.19.1-next.1';
|
|
7
7
|
|
|
8
8
|
exports.SDK_VERSION = SDK_VERSION;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.19.
|
|
1
|
+
export declare const SDK_VERSION = "2.19.1-next.1";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/embed-version.mjs — DO NOT EDIT.
|
|
2
2
|
// Source of truth: package.json. Regenerated on every SDK build so the
|
|
3
3
|
// SDK retains its own version identity when bundled into another package.
|
|
4
|
-
export const SDK_VERSION = '2.19.
|
|
4
|
+
export const SDK_VERSION = '2.19.1-next.1';
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var promises = require('node:fs/promises');
|
|
4
|
+
var os = require('node:os');
|
|
5
|
+
var path = require('node:path');
|
|
6
|
+
var node_crypto = require('node:crypto');
|
|
7
|
+
var errors = require('./errors.cjs');
|
|
8
|
+
|
|
9
|
+
const FILESYSTEM_CODES = new Set(['EACCES', 'EPERM', 'EROFS', 'ENOSPC', 'EDQUOT', 'ENOTDIR', 'ENOENT']);
|
|
10
|
+
class SdkStateDirectory {
|
|
11
|
+
configured;
|
|
12
|
+
selection;
|
|
13
|
+
constructor(configured) {
|
|
14
|
+
this.configured = configured;
|
|
15
|
+
}
|
|
16
|
+
get path() {
|
|
17
|
+
return this.selection?.path;
|
|
18
|
+
}
|
|
19
|
+
get ownsRoot() {
|
|
20
|
+
return this.selection?.owned === true;
|
|
21
|
+
}
|
|
22
|
+
async prepare(env) {
|
|
23
|
+
if (this.configured !== undefined && (typeof this.configured !== 'string' || !this.configured.trim())) {
|
|
24
|
+
throw new errors.SuperDocCliError('stateDir must be a non-empty path.', { code: 'INVALID_ARGUMENT' });
|
|
25
|
+
}
|
|
26
|
+
if (this.selection) {
|
|
27
|
+
await this.check(this.selection);
|
|
28
|
+
return this.selection.path;
|
|
29
|
+
}
|
|
30
|
+
const explicit = this.configured ?? (env.SUPERDOC_CLI_STATE_DIR || undefined);
|
|
31
|
+
if (explicit !== undefined) {
|
|
32
|
+
const selection = {
|
|
33
|
+
path: path.resolve(explicit),
|
|
34
|
+
source: this.configured !== undefined ? 'stateDir' : 'SUPERDOC_CLI_STATE_DIR',
|
|
35
|
+
owned: false,
|
|
36
|
+
};
|
|
37
|
+
await this.check(selection);
|
|
38
|
+
this.selection = selection;
|
|
39
|
+
return selection.path;
|
|
40
|
+
}
|
|
41
|
+
let home = process.platform === 'win32' ? env.USERPROFILE : env.HOME;
|
|
42
|
+
if (!home) {
|
|
43
|
+
try {
|
|
44
|
+
home = os.userInfo().homedir;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* A container UID need not have a passwd entry. */
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (home) {
|
|
51
|
+
const selection = { path: path.join(home, '.superdoc-cli', 'state', 'v1'), source: 'home', owned: false };
|
|
52
|
+
try {
|
|
53
|
+
if ((await promises.stat(home)).isDirectory()) {
|
|
54
|
+
await this.probe(selection.path);
|
|
55
|
+
this.selection = selection;
|
|
56
|
+
return selection.path;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* An unusable implicit home may use private temporary state. */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const temporary = process.platform === 'win32'
|
|
64
|
+
? env.TEMP || env.TMP || (env.SystemRoot ? path.join(env.SystemRoot, 'temp') : os.tmpdir())
|
|
65
|
+
: env.TMPDIR || env.TMP || env.TEMP || '/tmp';
|
|
66
|
+
let allocated;
|
|
67
|
+
try {
|
|
68
|
+
allocated = await promises.mkdtemp(path.join(path.resolve(temporary), 'superdoc-sdk-state-'));
|
|
69
|
+
const selection = { path: allocated, source: 'temporary', owned: true };
|
|
70
|
+
await this.probe(allocated);
|
|
71
|
+
this.selection = selection;
|
|
72
|
+
return allocated;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (allocated)
|
|
76
|
+
await promises.rm(allocated, { recursive: true, force: true });
|
|
77
|
+
throw this.failure({ path: path.resolve(temporary), source: 'temporary', owned: false }, error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async dispose() {
|
|
81
|
+
const selection = this.selection;
|
|
82
|
+
if (!selection)
|
|
83
|
+
return;
|
|
84
|
+
if (selection.owned)
|
|
85
|
+
await promises.rm(selection.path, { recursive: true, force: true });
|
|
86
|
+
this.selection = undefined;
|
|
87
|
+
}
|
|
88
|
+
mapOperationError(error) {
|
|
89
|
+
const selection = this.selection;
|
|
90
|
+
const code = error.message.match(/\b(E[A-Z]+)\b/)?.[1];
|
|
91
|
+
if (!selection || !code || !FILESYSTEM_CODES.has(code) || !error.message.includes(selection.path))
|
|
92
|
+
return error;
|
|
93
|
+
return this.failure(selection, { code });
|
|
94
|
+
}
|
|
95
|
+
async check(selection) {
|
|
96
|
+
try {
|
|
97
|
+
await this.probe(selection.path);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw this.failure(selection, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async probe(path$1) {
|
|
104
|
+
const contexts = path.join(path$1, 'contexts');
|
|
105
|
+
await promises.mkdir(contexts, { recursive: true, mode: 0o700 });
|
|
106
|
+
const probe = path.join(contexts, '.sdk-write-' + node_crypto.randomUUID());
|
|
107
|
+
const handle = await promises.open(probe, 'wx', 0o600);
|
|
108
|
+
try {
|
|
109
|
+
await handle.writeFile('state');
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await handle.close();
|
|
113
|
+
await promises.rm(probe, { force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
failure(selection, error) {
|
|
117
|
+
const fsCode = error?.code;
|
|
118
|
+
return new errors.SuperDocCliError('SuperDoc state directory is not writable: ' +
|
|
119
|
+
selection.path +
|
|
120
|
+
'. Configure stateDir or SUPERDOC_CLI_STATE_DIR with a writable directory.', {
|
|
121
|
+
code: 'STATE_DIRECTORY_UNWRITABLE',
|
|
122
|
+
details: {
|
|
123
|
+
path: selection.path,
|
|
124
|
+
source: selection.source,
|
|
125
|
+
fsCode,
|
|
126
|
+
remediation: 'Configure stateDir or SUPERDOC_CLI_STATE_DIR with a writable directory.',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
exports.SdkStateDirectory = SdkStateDirectory;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SuperDocCliError } from './errors.js';
|
|
2
|
+
export declare class SdkStateDirectory {
|
|
3
|
+
private readonly configured;
|
|
4
|
+
private selection;
|
|
5
|
+
constructor(configured: string | undefined);
|
|
6
|
+
get path(): string | undefined;
|
|
7
|
+
get ownsRoot(): boolean;
|
|
8
|
+
prepare(env: NodeJS.ProcessEnv): Promise<string>;
|
|
9
|
+
dispose(): Promise<void>;
|
|
10
|
+
mapOperationError(error: SuperDocCliError): SuperDocCliError;
|
|
11
|
+
private check;
|
|
12
|
+
private probe;
|
|
13
|
+
private failure;
|
|
14
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, open, rm, stat } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir, userInfo } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { SuperDocCliError } from './errors.js';
|
|
6
|
+
const FILESYSTEM_CODES = new Set(['EACCES', 'EPERM', 'EROFS', 'ENOSPC', 'EDQUOT', 'ENOTDIR', 'ENOENT']);
|
|
7
|
+
export class SdkStateDirectory {
|
|
8
|
+
configured;
|
|
9
|
+
selection;
|
|
10
|
+
constructor(configured) {
|
|
11
|
+
this.configured = configured;
|
|
12
|
+
}
|
|
13
|
+
get path() {
|
|
14
|
+
return this.selection?.path;
|
|
15
|
+
}
|
|
16
|
+
get ownsRoot() {
|
|
17
|
+
return this.selection?.owned === true;
|
|
18
|
+
}
|
|
19
|
+
async prepare(env) {
|
|
20
|
+
if (this.configured !== undefined && (typeof this.configured !== 'string' || !this.configured.trim())) {
|
|
21
|
+
throw new SuperDocCliError('stateDir must be a non-empty path.', { code: 'INVALID_ARGUMENT' });
|
|
22
|
+
}
|
|
23
|
+
if (this.selection) {
|
|
24
|
+
await this.check(this.selection);
|
|
25
|
+
return this.selection.path;
|
|
26
|
+
}
|
|
27
|
+
const explicit = this.configured ?? (env.SUPERDOC_CLI_STATE_DIR || undefined);
|
|
28
|
+
if (explicit !== undefined) {
|
|
29
|
+
const selection = {
|
|
30
|
+
path: resolve(explicit),
|
|
31
|
+
source: this.configured !== undefined ? 'stateDir' : 'SUPERDOC_CLI_STATE_DIR',
|
|
32
|
+
owned: false,
|
|
33
|
+
};
|
|
34
|
+
await this.check(selection);
|
|
35
|
+
this.selection = selection;
|
|
36
|
+
return selection.path;
|
|
37
|
+
}
|
|
38
|
+
let home = process.platform === 'win32' ? env.USERPROFILE : env.HOME;
|
|
39
|
+
if (!home) {
|
|
40
|
+
try {
|
|
41
|
+
home = userInfo().homedir;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* A container UID need not have a passwd entry. */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (home) {
|
|
48
|
+
const selection = { path: join(home, '.superdoc-cli', 'state', 'v1'), source: 'home', owned: false };
|
|
49
|
+
try {
|
|
50
|
+
if ((await stat(home)).isDirectory()) {
|
|
51
|
+
await this.probe(selection.path);
|
|
52
|
+
this.selection = selection;
|
|
53
|
+
return selection.path;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
/* An unusable implicit home may use private temporary state. */
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const temporary = process.platform === 'win32'
|
|
61
|
+
? env.TEMP || env.TMP || (env.SystemRoot ? join(env.SystemRoot, 'temp') : tmpdir())
|
|
62
|
+
: env.TMPDIR || env.TMP || env.TEMP || '/tmp';
|
|
63
|
+
let allocated;
|
|
64
|
+
try {
|
|
65
|
+
allocated = await mkdtemp(join(resolve(temporary), 'superdoc-sdk-state-'));
|
|
66
|
+
const selection = { path: allocated, source: 'temporary', owned: true };
|
|
67
|
+
await this.probe(allocated);
|
|
68
|
+
this.selection = selection;
|
|
69
|
+
return allocated;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (allocated)
|
|
73
|
+
await rm(allocated, { recursive: true, force: true });
|
|
74
|
+
throw this.failure({ path: resolve(temporary), source: 'temporary', owned: false }, error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async dispose() {
|
|
78
|
+
const selection = this.selection;
|
|
79
|
+
if (!selection)
|
|
80
|
+
return;
|
|
81
|
+
if (selection.owned)
|
|
82
|
+
await rm(selection.path, { recursive: true, force: true });
|
|
83
|
+
this.selection = undefined;
|
|
84
|
+
}
|
|
85
|
+
mapOperationError(error) {
|
|
86
|
+
const selection = this.selection;
|
|
87
|
+
const code = error.message.match(/\b(E[A-Z]+)\b/)?.[1];
|
|
88
|
+
if (!selection || !code || !FILESYSTEM_CODES.has(code) || !error.message.includes(selection.path))
|
|
89
|
+
return error;
|
|
90
|
+
return this.failure(selection, { code });
|
|
91
|
+
}
|
|
92
|
+
async check(selection) {
|
|
93
|
+
try {
|
|
94
|
+
await this.probe(selection.path);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw this.failure(selection, error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async probe(path) {
|
|
101
|
+
const contexts = join(path, 'contexts');
|
|
102
|
+
await mkdir(contexts, { recursive: true, mode: 0o700 });
|
|
103
|
+
const probe = join(contexts, '.sdk-write-' + randomUUID());
|
|
104
|
+
const handle = await open(probe, 'wx', 0o600);
|
|
105
|
+
try {
|
|
106
|
+
await handle.writeFile('state');
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await handle.close();
|
|
110
|
+
await rm(probe, { force: true });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
failure(selection, error) {
|
|
114
|
+
const fsCode = error?.code;
|
|
115
|
+
return new SuperDocCliError('SuperDoc state directory is not writable: ' +
|
|
116
|
+
selection.path +
|
|
117
|
+
'. Configure stateDir or SUPERDOC_CLI_STATE_DIR with a writable directory.', {
|
|
118
|
+
code: 'STATE_DIRECTORY_UNWRITABLE',
|
|
119
|
+
details: {
|
|
120
|
+
path: selection.path,
|
|
121
|
+
source: selection.source,
|
|
122
|
+
fsCode,
|
|
123
|
+
remediation: 'Configure stateDir or SUPERDOC_CLI_STATE_DIR with a writable directory.',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -45,6 +45,8 @@ export interface UserIdentity {
|
|
|
45
45
|
email?: string;
|
|
46
46
|
}
|
|
47
47
|
export interface SuperDocClientOptions {
|
|
48
|
+
/** Writable host state root. Takes precedence over SUPERDOC_CLI_STATE_DIR; persists after disposal. */
|
|
49
|
+
stateDir?: string;
|
|
48
50
|
env?: Record<string, string | undefined>;
|
|
49
51
|
/**
|
|
50
52
|
* Standalone document-host entry to use instead of the embedded CLI.
|