@fromeroc9/testform 1.0.3 → 1.0.5
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/dist/action/index.js +1 -1
- package/dist/action.js +60 -0
- package/dist/adapters/github.js +467 -0
- package/dist/adapters/resources.js +363 -0
- package/dist/cli/index.js +3 -3
- package/dist/commands/apply.js +390 -0
- package/dist/commands/destroy.js +85 -0
- package/dist/commands/diff.js +131 -0
- package/dist/commands/fmt.js +166 -0
- package/dist/commands/force-unlock.js +55 -0
- package/dist/commands/generate.js +143 -0
- package/dist/commands/graph.js +159 -0
- package/dist/commands/import.js +222 -0
- package/dist/commands/init.js +167 -0
- package/dist/commands/login.js +71 -0
- package/dist/commands/logout.js +20 -0
- package/dist/commands/plan.js +250 -0
- package/dist/commands/refresh.js +165 -0
- package/dist/commands/report.js +724 -0
- package/dist/commands/show.js +61 -0
- package/dist/commands/state.js +197 -0
- package/dist/commands/taint.js +49 -0
- package/dist/commands/validate.js +128 -0
- package/dist/commands/workspace.js +102 -0
- package/dist/const.js +105 -0
- package/dist/core/backends/azurerm.js +201 -0
- package/dist/core/backends/backend.js +2 -0
- package/dist/core/backends/gcs.js +200 -0
- package/dist/core/backends/local.js +162 -0
- package/dist/core/backends/s3.js +224 -0
- package/dist/core/command-context.js +59 -0
- package/dist/core/config.js +131 -0
- package/dist/core/credentials.js +53 -0
- package/dist/core/parser.js +62 -0
- package/dist/core/parsers/base-parser.js +215 -0
- package/dist/core/parsers/testcase-parser.js +115 -0
- package/dist/core/parsers/testplan-parser.js +41 -0
- package/dist/core/parsers/testrun-parser.js +43 -0
- package/dist/core/policy.js +341 -0
- package/dist/core/prompt.js +109 -0
- package/dist/core/state.js +185 -0
- package/dist/core/utils.js +94 -0
- package/dist/core/variables.js +108 -0
- package/dist/core/workspace.js +56 -0
- package/dist/help.js +797 -0
- package/dist/index.js +650 -0
- package/dist/logger.js +134 -0
- package/dist/notify.js +36 -0
- package/dist/types.js +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AzureRMBackend = void 0;
|
|
4
|
+
const storage_blob_1 = require("@azure/storage-blob");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const const_1 = require("../../const");
|
|
7
|
+
class AzureRMBackend {
|
|
8
|
+
config;
|
|
9
|
+
workspace;
|
|
10
|
+
containerClient;
|
|
11
|
+
blobClient;
|
|
12
|
+
currentLeaseId;
|
|
13
|
+
originalKey;
|
|
14
|
+
constructor(config, workspace = 'default') {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.workspace = workspace;
|
|
17
|
+
const connectionString = config.connection_string || process.env.AZURE_STORAGE_CONNECTION_STRING;
|
|
18
|
+
if (!connectionString) {
|
|
19
|
+
const error = new Error("AZURE_STORAGE_CONNECTION_STRING env variable is missing");
|
|
20
|
+
error.name = 'AzureRM Backend Configuration Error';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const blobServiceClient = storage_blob_1.BlobServiceClient.fromConnectionString(connectionString);
|
|
24
|
+
this.containerClient = blobServiceClient.getContainerClient(config.container_name);
|
|
25
|
+
this.config.key = this.config.key || const_1.FILE_STATE;
|
|
26
|
+
this.originalKey = this.config.key;
|
|
27
|
+
if (this.workspace !== 'default') {
|
|
28
|
+
this.config.key = `env:/${this.workspace}/${this.originalKey}`;
|
|
29
|
+
}
|
|
30
|
+
this.blobClient = this.containerClient.getBlockBlobClient(this.config.key);
|
|
31
|
+
}
|
|
32
|
+
async exists() {
|
|
33
|
+
try {
|
|
34
|
+
return await this.blobClient.exists();
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async read() {
|
|
41
|
+
try {
|
|
42
|
+
await this.containerClient.createIfNotExists();
|
|
43
|
+
const exists = await this.blobClient.exists();
|
|
44
|
+
if (!exists) {
|
|
45
|
+
return this.emptyState();
|
|
46
|
+
}
|
|
47
|
+
const response = await this.blobClient.download(0);
|
|
48
|
+
const raw = await this.streamToString(response.readableStreamBody);
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return this.emptyState();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async write(state) {
|
|
56
|
+
await this.containerClient.createIfNotExists();
|
|
57
|
+
const content = JSON.stringify(state, null, 2);
|
|
58
|
+
const options = { blobHTTPHeaders: { blobContentType: 'application/json' } };
|
|
59
|
+
if (this.currentLeaseId) {
|
|
60
|
+
options.conditions = { leaseId: this.currentLeaseId };
|
|
61
|
+
}
|
|
62
|
+
await this.blobClient.upload(content, content.length, options);
|
|
63
|
+
}
|
|
64
|
+
async lock(timeoutRaw) {
|
|
65
|
+
const timeoutMatch = timeoutRaw.match(/^(\d+)s$/);
|
|
66
|
+
const timeoutMs = timeoutMatch ? parseInt(timeoutMatch[1], 10) * 1000 : 0;
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
// Ensure blob exists before leasing
|
|
69
|
+
const exists = await this.blobClient.exists();
|
|
70
|
+
if (!exists) {
|
|
71
|
+
await this.write(this.emptyState());
|
|
72
|
+
}
|
|
73
|
+
const leaseClient = this.blobClient.getBlobLeaseClient();
|
|
74
|
+
while (true) {
|
|
75
|
+
try {
|
|
76
|
+
// -1 means infinite lease
|
|
77
|
+
const response = await leaseClient.acquireLease(-1);
|
|
78
|
+
this.currentLeaseId = response.leaseId;
|
|
79
|
+
// Write lock metadata
|
|
80
|
+
try {
|
|
81
|
+
await this.blobClient.setMetadata({
|
|
82
|
+
id: (0, crypto_1.randomUUID)(),
|
|
83
|
+
operation: 'Operation',
|
|
84
|
+
who: process.env.USER || 'unknown',
|
|
85
|
+
created: new Date().toISOString()
|
|
86
|
+
}, { conditions: { leaseId: this.currentLeaseId } });
|
|
87
|
+
}
|
|
88
|
+
catch (metaErr) {
|
|
89
|
+
// Floci emulator bug workaround: it returns 201 Created instead of 200 OK
|
|
90
|
+
// for setMetadata, which crashes the strict Azure SDK.
|
|
91
|
+
if (metaErr.statusCode !== 201) {
|
|
92
|
+
throw metaErr;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err.statusCode === 409 || err.code === 'LeaseAlreadyPresent') {
|
|
99
|
+
if (Date.now() - start >= timeoutMs) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async unlock() {
|
|
111
|
+
if (!this.currentLeaseId)
|
|
112
|
+
return true;
|
|
113
|
+
try {
|
|
114
|
+
const leaseClient = this.blobClient.getBlobLeaseClient(this.currentLeaseId);
|
|
115
|
+
await leaseClient.releaseLease();
|
|
116
|
+
this.currentLeaseId = undefined;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async forceUnlock(lockId) {
|
|
124
|
+
try {
|
|
125
|
+
const props = await this.blobClient.getProperties();
|
|
126
|
+
const currentLockId = props.metadata?.id;
|
|
127
|
+
if (props.leaseState === 'available') {
|
|
128
|
+
return { success: false, error: 'No lock exists for the given state.' };
|
|
129
|
+
}
|
|
130
|
+
if (currentLockId !== lockId) {
|
|
131
|
+
return { success: false, currentLockId: currentLockId || 'unknown' };
|
|
132
|
+
}
|
|
133
|
+
// Break the lease
|
|
134
|
+
const leaseClient = this.blobClient.getBlobLeaseClient();
|
|
135
|
+
await leaseClient.breakLease(0);
|
|
136
|
+
return { success: true };
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return { success: false, error: `Failed to force unlock: ${err.message}` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async isLocked() {
|
|
143
|
+
try {
|
|
144
|
+
const props = await this.blobClient.getProperties();
|
|
145
|
+
return props.leaseState !== 'available';
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
emptyState() {
|
|
152
|
+
return {
|
|
153
|
+
version: const_1.VERSION_STATE,
|
|
154
|
+
serial: 0,
|
|
155
|
+
lineage: (0, crypto_1.randomUUID)(),
|
|
156
|
+
lastSync: '',
|
|
157
|
+
resources: [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async streamToString(readableStream) {
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
162
|
+
const chunks = [];
|
|
163
|
+
readableStream.on("data", (data) => {
|
|
164
|
+
chunks.push(data.toString());
|
|
165
|
+
});
|
|
166
|
+
readableStream.on("end", () => {
|
|
167
|
+
resolve(chunks.join(""));
|
|
168
|
+
});
|
|
169
|
+
readableStream.on("error", reject);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async listWorkspaces() {
|
|
173
|
+
const workspaces = new Set(['default']);
|
|
174
|
+
try {
|
|
175
|
+
for await (const blob of this.containerClient.listBlobsFlat({ prefix: 'env:/' })) {
|
|
176
|
+
const match = blob.name.match(/^env:\/([^\/]+)\//);
|
|
177
|
+
if (match) {
|
|
178
|
+
workspaces.add(match[1]);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
// fallback
|
|
184
|
+
}
|
|
185
|
+
return Array.from(workspaces);
|
|
186
|
+
}
|
|
187
|
+
async deleteWorkspace(name) {
|
|
188
|
+
if (name === 'default')
|
|
189
|
+
return false;
|
|
190
|
+
try {
|
|
191
|
+
const targetKey = `env:/${name}/${this.originalKey}`;
|
|
192
|
+
const targetClient = this.containerClient.getBlockBlobClient(targetKey);
|
|
193
|
+
await targetClient.deleteIfExists();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
exports.AzureRMBackend = AzureRMBackend;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GCSBackend = void 0;
|
|
4
|
+
const storage_1 = require("@google-cloud/storage");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
6
|
+
const const_1 = require("../../const");
|
|
7
|
+
class GCSBackend {
|
|
8
|
+
config;
|
|
9
|
+
workspace;
|
|
10
|
+
storage;
|
|
11
|
+
bucketClient;
|
|
12
|
+
stateKey;
|
|
13
|
+
lockKey;
|
|
14
|
+
lockId;
|
|
15
|
+
constructor(config, workspace = 'default') {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.workspace = workspace;
|
|
18
|
+
const storageOptions = {};
|
|
19
|
+
if (config.credentials) {
|
|
20
|
+
storageOptions.keyFilename = config.credentials;
|
|
21
|
+
}
|
|
22
|
+
this.storage = new storage_1.Storage(storageOptions);
|
|
23
|
+
this.bucketClient = this.storage.bucket(config.bucket);
|
|
24
|
+
let prefix = this.config.prefix || '';
|
|
25
|
+
if (prefix && !prefix.endsWith('/')) {
|
|
26
|
+
prefix += '/';
|
|
27
|
+
}
|
|
28
|
+
const originalKey = const_1.FILE_STATE;
|
|
29
|
+
if (this.workspace !== 'default') {
|
|
30
|
+
this.stateKey = `${prefix}env:/${this.workspace}/${originalKey}`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.stateKey = `${prefix}${originalKey}`;
|
|
34
|
+
}
|
|
35
|
+
this.lockKey = `${this.stateKey}.tflock`;
|
|
36
|
+
}
|
|
37
|
+
emptyState() {
|
|
38
|
+
return {
|
|
39
|
+
version: const_1.VERSION_STATE,
|
|
40
|
+
serial: 0,
|
|
41
|
+
lineage: (0, crypto_1.randomUUID)(),
|
|
42
|
+
lastSync: '',
|
|
43
|
+
resources: []
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
async exists() {
|
|
47
|
+
try {
|
|
48
|
+
const [exists] = await this.bucketClient.file(this.config.prefix).exists();
|
|
49
|
+
return exists;
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async read() {
|
|
56
|
+
try {
|
|
57
|
+
const file = this.bucketClient.file(this.stateKey);
|
|
58
|
+
const [exists] = await file.exists();
|
|
59
|
+
if (!exists) {
|
|
60
|
+
return this.emptyState();
|
|
61
|
+
}
|
|
62
|
+
const [content] = await file.download();
|
|
63
|
+
return JSON.parse(content.toString('utf8'));
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return this.emptyState();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async write(state) {
|
|
70
|
+
const file = this.bucketClient.file(this.stateKey);
|
|
71
|
+
const content = JSON.stringify(state, null, 2);
|
|
72
|
+
await file.save(content, {
|
|
73
|
+
contentType: 'application/json'
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async lock(timeoutRaw) {
|
|
77
|
+
const timeoutMatch = timeoutRaw.match(/^(\d+)s$/);
|
|
78
|
+
const timeoutMs = timeoutMatch ? parseInt(timeoutMatch[1], 10) * 1000 : 0;
|
|
79
|
+
const start = Date.now();
|
|
80
|
+
const file = this.bucketClient.file(this.lockKey);
|
|
81
|
+
const lockPayload = JSON.stringify({
|
|
82
|
+
id: (0, crypto_1.randomUUID)(),
|
|
83
|
+
operation: 'Operation',
|
|
84
|
+
who: process.env.USER || 'unknown',
|
|
85
|
+
created: new Date().toISOString()
|
|
86
|
+
});
|
|
87
|
+
while (true) {
|
|
88
|
+
try {
|
|
89
|
+
// ifGenerationMatch: 0 ensures the file is created ONLY if it does not already exist.
|
|
90
|
+
await file.save(lockPayload, {
|
|
91
|
+
preconditionOpts: { ifGenerationMatch: 0 }
|
|
92
|
+
});
|
|
93
|
+
this.lockId = lockPayload;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// 412 Precondition Failed means the file already exists (lock is acquired by someone else)
|
|
98
|
+
if (err.code === 412 || err.code === '412') {
|
|
99
|
+
if (Date.now() - start >= timeoutMs) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async unlock() {
|
|
111
|
+
if (!this.lockId)
|
|
112
|
+
return true;
|
|
113
|
+
const file = this.bucketClient.file(this.lockKey);
|
|
114
|
+
try {
|
|
115
|
+
const [exists] = await file.exists();
|
|
116
|
+
if (exists) {
|
|
117
|
+
await file.delete();
|
|
118
|
+
}
|
|
119
|
+
this.lockId = undefined;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async forceUnlock(id) {
|
|
127
|
+
const file = this.bucketClient.file(this.lockKey);
|
|
128
|
+
try {
|
|
129
|
+
const [exists] = await file.exists();
|
|
130
|
+
if (!exists) {
|
|
131
|
+
return { success: false, error: 'No lock exists for the given state.' };
|
|
132
|
+
}
|
|
133
|
+
const [content] = await file.download();
|
|
134
|
+
const info = JSON.parse(content.toString('utf8'));
|
|
135
|
+
if (info.id !== id) {
|
|
136
|
+
return { success: false, currentLockId: info.id };
|
|
137
|
+
}
|
|
138
|
+
await file.delete();
|
|
139
|
+
this.lockId = undefined;
|
|
140
|
+
return { success: true };
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
return { success: false, error: e.message };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async isLocked() {
|
|
147
|
+
const file = this.bucketClient.file(this.lockKey);
|
|
148
|
+
try {
|
|
149
|
+
const [exists] = await file.exists();
|
|
150
|
+
return exists;
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async listWorkspaces() {
|
|
157
|
+
const workspaces = new Set(['default']);
|
|
158
|
+
try {
|
|
159
|
+
let prefix = this.config.prefix || '';
|
|
160
|
+
if (prefix && !prefix.endsWith('/')) {
|
|
161
|
+
prefix += '/';
|
|
162
|
+
}
|
|
163
|
+
const searchPrefix = `${prefix}env:/`;
|
|
164
|
+
const [files] = await this.bucketClient.getFiles({ prefix: searchPrefix });
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
const relName = file.name.substring(searchPrefix.length);
|
|
167
|
+
const match = relName.match(/^([^\/]+)\//);
|
|
168
|
+
if (match) {
|
|
169
|
+
workspaces.add(match[1]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
// fallback
|
|
175
|
+
}
|
|
176
|
+
return Array.from(workspaces);
|
|
177
|
+
}
|
|
178
|
+
async deleteWorkspace(name) {
|
|
179
|
+
if (name === 'default')
|
|
180
|
+
return false;
|
|
181
|
+
try {
|
|
182
|
+
let prefix = this.config.prefix || '';
|
|
183
|
+
if (prefix && !prefix.endsWith('/')) {
|
|
184
|
+
prefix += '/';
|
|
185
|
+
}
|
|
186
|
+
const targetKey = `${prefix}env:/${name}/${const_1.FILE_STATE}`;
|
|
187
|
+
const targetClient = this.bucketClient.file(targetKey);
|
|
188
|
+
const [exists] = await targetClient.exists();
|
|
189
|
+
if (exists) {
|
|
190
|
+
await targetClient.delete();
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
exports.GCSBackend = GCSBackend;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalBackend = void 0;
|
|
4
|
+
const path_1 = require("path");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const const_1 = require("../../const");
|
|
8
|
+
class LocalBackend {
|
|
9
|
+
dir;
|
|
10
|
+
customStatePath;
|
|
11
|
+
customBackupPath;
|
|
12
|
+
workspace;
|
|
13
|
+
constructor(dir, customStatePath, customBackupPath, workspace = 'default') {
|
|
14
|
+
this.dir = dir;
|
|
15
|
+
this.customStatePath = customStatePath;
|
|
16
|
+
this.customBackupPath = customBackupPath;
|
|
17
|
+
this.workspace = workspace;
|
|
18
|
+
}
|
|
19
|
+
getWorkspaceStatePath() {
|
|
20
|
+
if (this.customStatePath) {
|
|
21
|
+
return (0, path_1.resolve)(this.dir, this.customStatePath);
|
|
22
|
+
}
|
|
23
|
+
if (this.workspace !== 'default') {
|
|
24
|
+
return (0, path_1.resolve)(this.dir, `${const_1.FILE_STATE}.d`, this.workspace, const_1.FILE_STATE);
|
|
25
|
+
}
|
|
26
|
+
return (0, path_1.resolve)(this.dir, const_1.FILE_STATE);
|
|
27
|
+
}
|
|
28
|
+
resolvePath() {
|
|
29
|
+
const path = this.getWorkspaceStatePath();
|
|
30
|
+
if ((0, fs_1.existsSync)(path))
|
|
31
|
+
return path;
|
|
32
|
+
if (this.workspace === 'default') {
|
|
33
|
+
const cwd = (0, path_1.resolve)(this.dir, const_1.FILE_STATE);
|
|
34
|
+
if ((0, fs_1.existsSync)(cwd))
|
|
35
|
+
return cwd;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
lockPath() {
|
|
40
|
+
const path = this.resolvePath() ?? this.getWorkspaceStatePath();
|
|
41
|
+
return `${path}.lock`;
|
|
42
|
+
}
|
|
43
|
+
async exists() {
|
|
44
|
+
return this.resolvePath() !== null;
|
|
45
|
+
}
|
|
46
|
+
async read() {
|
|
47
|
+
const path = this.resolvePath();
|
|
48
|
+
if (!path || !(0, fs_1.existsSync)(path)) {
|
|
49
|
+
return {
|
|
50
|
+
version: const_1.VERSION_STATE,
|
|
51
|
+
serial: 0,
|
|
52
|
+
lineage: (0, crypto_1.randomUUID)(),
|
|
53
|
+
lastSync: '',
|
|
54
|
+
resources: [],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const raw = await fs_1.promises.readFile(path, "utf-8");
|
|
58
|
+
return JSON.parse(raw);
|
|
59
|
+
}
|
|
60
|
+
async write(state) {
|
|
61
|
+
const path = this.resolvePath() ?? this.getWorkspaceStatePath();
|
|
62
|
+
// Ensure directory exists
|
|
63
|
+
const dirPath = (0, path_1.resolve)(path, '..');
|
|
64
|
+
if (!(0, fs_1.existsSync)(dirPath)) {
|
|
65
|
+
await fs_1.promises.mkdir(dirPath, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
const backupPath = this.customBackupPath ?
|
|
68
|
+
(0, path_1.resolve)(this.dir, this.customBackupPath) :
|
|
69
|
+
`${path}.backup`;
|
|
70
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
71
|
+
await fs_1.promises.copyFile(path, backupPath);
|
|
72
|
+
}
|
|
73
|
+
await fs_1.promises.writeFile(path, JSON.stringify(state, null, 2), "utf-8");
|
|
74
|
+
}
|
|
75
|
+
async lock(timeoutRaw) {
|
|
76
|
+
const timeoutMatch = timeoutRaw.match(/^(\d+)s$/);
|
|
77
|
+
const timeoutMs = timeoutMatch ? parseInt(timeoutMatch[1], 10) * 1000 : 0;
|
|
78
|
+
const lockFile = this.lockPath();
|
|
79
|
+
const start = Date.now();
|
|
80
|
+
while (true) {
|
|
81
|
+
try {
|
|
82
|
+
// wx flag: open for writing, fails if the file exists
|
|
83
|
+
await fs_1.promises.writeFile(lockFile, JSON.stringify({
|
|
84
|
+
id: (0, crypto_1.randomUUID)(),
|
|
85
|
+
operation: 'Operation',
|
|
86
|
+
who: process.env.USER || 'unknown',
|
|
87
|
+
created: new Date().toISOString()
|
|
88
|
+
}, null, 2), { flag: 'wx' });
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
if (err.code === 'EEXIST') {
|
|
93
|
+
if (Date.now() - start >= timeoutMs) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
throw err;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async unlock() {
|
|
105
|
+
const lockFile = this.lockPath();
|
|
106
|
+
if ((0, fs_1.existsSync)(lockFile)) {
|
|
107
|
+
try {
|
|
108
|
+
await fs_1.promises.unlink(lockFile);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
async forceUnlock(lockId) {
|
|
118
|
+
const lockFile = this.lockPath();
|
|
119
|
+
if (!(0, fs_1.existsSync)(lockFile)) {
|
|
120
|
+
return { success: false, error: `No lock exists for the given state. (Path checked: ${lockFile})` };
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const raw = await fs_1.promises.readFile(lockFile, 'utf-8');
|
|
124
|
+
const info = JSON.parse(raw);
|
|
125
|
+
if (info.id !== lockId) {
|
|
126
|
+
return { success: false, currentLockId: info.id };
|
|
127
|
+
}
|
|
128
|
+
await fs_1.promises.unlink(lockFile);
|
|
129
|
+
return { success: true };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
return { success: false, error: `The lock file could not be read or parsed.` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async isLocked() {
|
|
136
|
+
return (0, fs_1.existsSync)(this.lockPath());
|
|
137
|
+
}
|
|
138
|
+
async listWorkspaces() {
|
|
139
|
+
const workspaces = ['default'];
|
|
140
|
+
const dPath = (0, path_1.resolve)(this.dir, `${const_1.FILE_STATE}.d`);
|
|
141
|
+
if ((0, fs_1.existsSync)(dPath)) {
|
|
142
|
+
const entries = await fs_1.promises.readdir(dPath, { withFileTypes: true });
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
workspaces.push(entry.name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return workspaces;
|
|
150
|
+
}
|
|
151
|
+
async deleteWorkspace(name) {
|
|
152
|
+
if (name === 'default')
|
|
153
|
+
return false;
|
|
154
|
+
const wPath = (0, path_1.resolve)(this.dir, `${const_1.FILE_STATE}.d`, name);
|
|
155
|
+
if ((0, fs_1.existsSync)(wPath)) {
|
|
156
|
+
await fs_1.promises.rm(wPath, { recursive: true, force: true });
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exports.LocalBackend = LocalBackend;
|