@skyramp/skyramp 1.3.8 → 1.3.10
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/package.json +3 -2
- package/src/classes/SkyrampClient.d.ts +2 -0
- package/src/classes/SkyrampClient.js +4 -44
- package/src/classes/SmartPlaywright.js +41 -11
- package/src/index.d.ts +1 -0
- package/src/index.js +16 -0
- package/src/utils.d.ts +2 -2
- package/src/utils.js +14 -3
- package/src/workspace.d.ts +119 -0
- package/src/workspace.js +448 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/skyramp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"description": "module for leveraging skyramp cli functionality",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "eslint 'src/**/*.js' 'src/**/*.ts' --fix",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"@aws-sdk/client-s3": "^3.812.0",
|
|
25
25
|
"fs": "^0.0.1-security",
|
|
26
26
|
"js-yaml": "^4.1.0",
|
|
27
|
-
"koffi": "2.5.12"
|
|
27
|
+
"koffi": "2.5.12",
|
|
28
|
+
"zod": "^3.25.3"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
|
@@ -126,6 +126,8 @@ interface GenerateRestTestOptions {
|
|
|
126
126
|
playwrightViewportSize?: string;
|
|
127
127
|
playwrightStoragePath?: string;
|
|
128
128
|
playwrightSaveStoragePath?: string;
|
|
129
|
+
browser?: string;
|
|
130
|
+
device?: string;
|
|
129
131
|
loadCount?: string;
|
|
130
132
|
loadDuration?: string;
|
|
131
133
|
loadNumThreads?: string;
|
|
@@ -15,11 +15,6 @@ const testerInfoType = koffi.struct({
|
|
|
15
15
|
error: 'char*',
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
const testerGenerateType = koffi.struct({
|
|
19
|
-
generated_files: 'char*',
|
|
20
|
-
error: 'char*',
|
|
21
|
-
});
|
|
22
|
-
|
|
23
18
|
const contractResponseType = koffi.struct({
|
|
24
19
|
response: 'char*',
|
|
25
20
|
error: 'char*',
|
|
@@ -55,9 +50,8 @@ const applyMockObjectWrapper = lib.func('applyMockObjectWrapper', 'string', ['st
|
|
|
55
50
|
const initTargetWrapper = lib.func('initTargetWrapper', 'string', ['string']);
|
|
56
51
|
const deployTargetWrapper = lib.func('deployTargetWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'bool']);
|
|
57
52
|
const deleteTargetWrapper = lib.func('deleteTargetWrapper', 'string', ['string', 'string', 'string', 'string', 'string']);
|
|
58
|
-
const runTesterGenerateRestWrapper = lib.func('runTesterGenerateRestWrapper', testerGenerateType, ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'int', 'bool', 'bool', 'bool']);
|
|
59
53
|
|
|
60
|
-
const generateRestTestWrapper = lib.func('generateRestTestWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'bool', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'string', 'bool', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'string', 'string']);
|
|
54
|
+
const generateRestTestWrapper = lib.func('generateRestTestWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'bool', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'string', 'bool', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'string', 'string']);
|
|
61
55
|
const generateRestMockWrapper = lib.func('generateRestMockWrapper', 'string', ['string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'string', 'bool', 'bool', 'string', 'string', 'string', 'string', 'string', 'string']);
|
|
62
56
|
const traceCollectWrapper = lib.func('traceCollectWrapper', 'string', ['string', 'string', 'bool', 'string', 'string']);
|
|
63
57
|
const analyzeOpenapiWrapper = lib.func('analyzeOpenapiWrapper', 'string', ['string', 'string']);
|
|
@@ -104,8 +98,6 @@ class SkyrampClient {
|
|
|
104
98
|
this.local_image = false;
|
|
105
99
|
this.timestamp = Date.now();
|
|
106
100
|
|
|
107
|
-
checkForUpdate("npm")
|
|
108
|
-
|
|
109
101
|
if (typeof kubeconfigPathOrOptions === 'object') {
|
|
110
102
|
const options = kubeconfigPathOrOptions;
|
|
111
103
|
this.workerNamespaces = [];
|
|
@@ -666,41 +658,6 @@ class SkyrampClient {
|
|
|
666
658
|
return mockDescription;
|
|
667
659
|
}
|
|
668
660
|
|
|
669
|
-
/**
|
|
670
|
-
* Generates test scenarios based on the provided parameters.
|
|
671
|
-
*
|
|
672
|
-
* @param {Protocol} protocol - The protocol to be used.
|
|
673
|
-
* @param {string} apiSchemaPath - The path to the API schema.
|
|
674
|
-
* @param {string} alias - The alias for the generated tests.
|
|
675
|
-
* @param {string} endpointPath - Endpoint REST path to filter for.
|
|
676
|
-
* @param {Language} language - The programming language for the generated tests.
|
|
677
|
-
* @param {string} tag - Tag to filter OpenAPI endpoint paths for.
|
|
678
|
-
* @param {string} sampleRequestPath - The path to the sample request.
|
|
679
|
-
* @param {int} port - The port number.
|
|
680
|
-
* @param {boolean} generateRobot - Whether to generate robot tests.
|
|
681
|
-
* @param {boolean} functionalScenario - Whether to generate functional scenarios.
|
|
682
|
-
* @param {boolean} negativeScenario - Whether to generate negative scenarios.
|
|
683
|
-
* @returns {Promise<string[]>} A promise that resolves with a list of generated test file paths.
|
|
684
|
-
*/
|
|
685
|
-
async testerGenerate(protocol, apiSchemaPath, alias, endpointPath, language, tag, sampleRequestPath, port, generateRobot, functionalScenario, negativeScenario) {
|
|
686
|
-
return new Promise((resolve, reject) => {
|
|
687
|
-
runTesterGenerateRestWrapper.async(protocol, apiSchemaPath, alias, endpointPath, language, tag, sampleRequestPath, this.projectPath, port, generateRobot, functionalScenario, negativeScenario, (err, res) => {
|
|
688
|
-
if (err) {
|
|
689
|
-
console.error(`Error generating tests: ${err.name} - ${err.message}`);
|
|
690
|
-
reject(err);
|
|
691
|
-
} else if (res.error) {
|
|
692
|
-
console.error(`Error generating tests: ${res.error}`);
|
|
693
|
-
reject(new Error(res.error));
|
|
694
|
-
} else {
|
|
695
|
-
console.log(`Test generation completed successfully. Generated files: ${res.generated_files}`);
|
|
696
|
-
|
|
697
|
-
const generatedTestNames = res.generated_files.split(',');
|
|
698
|
-
resolve(generatedTestNames);
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
|
|
704
661
|
/**
|
|
705
662
|
* Sends a request to a Skyramp worker using the V2 API
|
|
706
663
|
* @param {Object} options - The options for sending the request
|
|
@@ -868,6 +825,8 @@ class SkyrampClient {
|
|
|
868
825
|
options.playwrightViewportSize || "",
|
|
869
826
|
options.playwrightStoragePath || "",
|
|
870
827
|
options.playwrightSaveStoragePath || "",
|
|
828
|
+
options.browser || "",
|
|
829
|
+
options.device || "",
|
|
871
830
|
options.loadCount || "0",
|
|
872
831
|
options.loadDuration || "0",
|
|
873
832
|
options.loadNumThreads || "0",
|
|
@@ -1054,6 +1013,7 @@ class SkyrampClient {
|
|
|
1054
1013
|
* @returns {Promise<string>} A promise that resolves with the initialization output message.
|
|
1055
1014
|
*/
|
|
1056
1015
|
async initAgent(options) {
|
|
1016
|
+
await checkForUpdate("npm");
|
|
1057
1017
|
return new Promise((resolve, reject) => {
|
|
1058
1018
|
initAgentWrapper.async(
|
|
1059
1019
|
options.version || "",
|
|
@@ -290,13 +290,30 @@ class SkyrampPlaywrightLocator {
|
|
|
290
290
|
});
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
/**
|
|
294
|
+
* execute locator's actions
|
|
295
|
+
*
|
|
296
|
+
* @param {bool} reducedTimeout - it forces 3sec timeout when set to true
|
|
297
|
+
*/
|
|
298
|
+
async execute(reducedTimeout) {
|
|
294
299
|
debug(` execute ${ this._locator}.${this.execFname} ${this.execParam ?? ''} ${this.execArgs ?? ''}`)
|
|
300
|
+
var newArgs = this.execArgs
|
|
301
|
+
if (reducedTimeout !== undefined && reducedTimeout) {
|
|
302
|
+
if (this.execArgs !== null && this.execArgs !== undefined) {
|
|
303
|
+
// we can safetly assume option is the first element in the execArgs
|
|
304
|
+
newArgs = [ { ...this.execArgs[0], timeout: 3000}, ...this.execArgs.slice(1)];
|
|
305
|
+
debug(' reduce timeout', this.execArgs, newArgs)
|
|
306
|
+
} else {
|
|
307
|
+
newArgs = [{ timeout: 3000 }];
|
|
308
|
+
debug(' reduce timeout', newArgs)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
295
312
|
const func = this._locator[this.execFname];
|
|
296
313
|
if (this.execParam !== null && this.execParam !== undefined) {
|
|
297
|
-
return func.call(this._locator, this.execParam, ...
|
|
314
|
+
return func.call(this._locator, this.execParam, ...newArgs);
|
|
298
315
|
} else {
|
|
299
|
-
return func.call(this._locator, ...
|
|
316
|
+
return func.call(this._locator, ...newArgs);
|
|
300
317
|
}
|
|
301
318
|
}
|
|
302
319
|
|
|
@@ -339,7 +356,7 @@ class SkyrampPlaywrightLocator {
|
|
|
339
356
|
|
|
340
357
|
newPrevHydrationErrorMsg() {
|
|
341
358
|
return `Cannot find locator ${this._locator} and likely a hydration issue on ${this._previousLocator._locator}.\n` +
|
|
342
|
-
`Please add enough waitForTimeout() on ${this.
|
|
359
|
+
`Please add enough waitForTimeout() on ${this._previousLocator._locator}`;
|
|
343
360
|
}
|
|
344
361
|
|
|
345
362
|
newMultiLocatorErrorMsg() {
|
|
@@ -392,16 +409,16 @@ class SkyrampPlaywrightLocator {
|
|
|
392
409
|
await this.wait(defaultWaitForTimeout);
|
|
393
410
|
|
|
394
411
|
// Is this really necessary?
|
|
395
|
-
await this.execute().then(result => {
|
|
412
|
+
await this.execute(true).then(result => {
|
|
396
413
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
397
414
|
}).catch(() => {
|
|
398
415
|
debug(` failed second time and execute previous locator ${this._previousLocator._locator} again`);
|
|
399
|
-
this._previousLocator.execute();
|
|
416
|
+
this._previousLocator.execute(true);
|
|
400
417
|
}).catch(() => {
|
|
401
418
|
debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
|
|
402
419
|
});
|
|
403
420
|
|
|
404
|
-
return this.execute().catch(newError => {
|
|
421
|
+
return this.execute(true).catch(newError => {
|
|
405
422
|
debug(` third attempt on ${this._locator} failed ${newError.name}`);
|
|
406
423
|
if (newError.name == "TimeoutError") {
|
|
407
424
|
// this hadn't happened yet. we need to validate if this is indeed hydration case
|
|
@@ -455,7 +472,7 @@ class SkyrampPlaywrightLocator {
|
|
|
455
472
|
const previousCount = await this._previousLocator.count();
|
|
456
473
|
debug(` re-execute the previous one ${this._previousLocator._locator}, new locator count = ${previousCount}, ${currentUrl}`);
|
|
457
474
|
// re-execute the previous locator
|
|
458
|
-
await this._previousLocator.execute().catch(() => {
|
|
475
|
+
await this._previousLocator.execute(true).catch(() => {
|
|
459
476
|
// log the failure but continues to the current one
|
|
460
477
|
debug(` failed to execute previous locator ${this._previousLocator._locator} again, continue`);
|
|
461
478
|
});
|
|
@@ -470,7 +487,7 @@ class SkyrampPlaywrightLocator {
|
|
|
470
487
|
debug(` ${this._locator} failed at first try. attempting again with some timeout`);
|
|
471
488
|
// wait for some time and re execute
|
|
472
489
|
await this.wait(defaultWaitForTimeout);
|
|
473
|
-
return this.execute().then(result => {
|
|
490
|
+
return this.execute(true).then(result => {
|
|
474
491
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
475
492
|
}).catch(newError => {
|
|
476
493
|
return this._retryWithLLM(newError, this.newPrevHydrationErrorMsg());
|
|
@@ -504,7 +521,7 @@ class SkyrampPlaywrightLocator {
|
|
|
504
521
|
if (error.name == "TimeoutError") {
|
|
505
522
|
debug(`${this._locator} failed at first try. attempting again with some timeout`);
|
|
506
523
|
await this.wait(defaultWaitForTimeout);
|
|
507
|
-
return this.execute().then(result=> {
|
|
524
|
+
return this.execute(true).then(result=> {
|
|
508
525
|
return this._skyrampPage.checkNavigation(currentUrl, result);
|
|
509
526
|
}).catch(newError => {
|
|
510
527
|
if (newError.name == "TimeoutError") {
|
|
@@ -717,7 +734,9 @@ class SkyrampPlaywrightLocator {
|
|
|
717
734
|
|
|
718
735
|
class SkyrampPlaywrightPage {
|
|
719
736
|
constructor(page, testInfo) {
|
|
720
|
-
checkForUpdate("npm")
|
|
737
|
+
checkForUpdate("npm").catch((error) => {
|
|
738
|
+
console.error('checkForUpdate("npm") failed:', error);
|
|
739
|
+
});
|
|
721
740
|
|
|
722
741
|
this._page = page;
|
|
723
742
|
this._testInfo = testInfo; // Store testInfo for screenshot auto-baseline
|
|
@@ -822,6 +841,17 @@ class SkyrampPlaywrightPage {
|
|
|
822
841
|
return this.newSkyrampPlaywrightLocator(originalLocator, alt, options);
|
|
823
842
|
}
|
|
824
843
|
|
|
844
|
+
async waitForResponse(arg, options) {
|
|
845
|
+
// we increase timeout for waitForResponse to 30 sec
|
|
846
|
+
// so that in case smart selector is required
|
|
847
|
+
var newOptions = { timeout: 60000 }
|
|
848
|
+
if (options !== null && options !== undefined) {
|
|
849
|
+
newOptions = { ...options, timeout: 60000 }
|
|
850
|
+
}
|
|
851
|
+
debug(` waitforresponse`, newOptions)
|
|
852
|
+
return this._page.waitForResponse(arg, newOptions)
|
|
853
|
+
}
|
|
854
|
+
|
|
825
855
|
async goto(url, options) {
|
|
826
856
|
const transformedUrl = transformUrlForDocker(url);
|
|
827
857
|
const result = await this._page.goto(transformedUrl, options);
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -20,6 +20,15 @@ const MockV2 = require('./classes/MockV2');
|
|
|
20
20
|
const { getValue, getResponseValue, checkSchema, iterate, pushToolEvent } = require('./utils');
|
|
21
21
|
const { checkStatusCode } = require('./function');
|
|
22
22
|
const { newSkyrampPlaywrightPage, expect } = require('./classes/SmartPlaywright');
|
|
23
|
+
const {
|
|
24
|
+
workspaceConfigSchema,
|
|
25
|
+
serviceSchema,
|
|
26
|
+
WorkspaceConfigManager,
|
|
27
|
+
validateWorkspaceConfig,
|
|
28
|
+
createDefaultConfig,
|
|
29
|
+
WORKSPACE_DIR,
|
|
30
|
+
WORKSPACE_FILENAME,
|
|
31
|
+
} = require('./workspace');
|
|
23
32
|
|
|
24
33
|
module.exports = {
|
|
25
34
|
SkyrampClient,
|
|
@@ -50,4 +59,11 @@ module.exports = {
|
|
|
50
59
|
pushToolEvent,
|
|
51
60
|
newSkyrampPlaywrightPage,
|
|
52
61
|
expect,
|
|
62
|
+
workspaceConfigSchema,
|
|
63
|
+
serviceSchema,
|
|
64
|
+
WorkspaceConfigManager,
|
|
65
|
+
validateWorkspaceConfig,
|
|
66
|
+
createDefaultConfig,
|
|
67
|
+
WORKSPACE_FILENAME,
|
|
68
|
+
WORKSPACE_DIR,
|
|
53
69
|
}
|
package/src/utils.d.ts
CHANGED
|
@@ -93,12 +93,12 @@ export function iterate(jsonInput: object): object;
|
|
|
93
93
|
* @param {string} error - error message if it failed
|
|
94
94
|
* @param {Object} params - any parameters associated with the tool call. Disctionary
|
|
95
95
|
*/
|
|
96
|
-
export function pushToolEvent(entryPoint: string, toolName: string, err: string, params: object): void
|
|
96
|
+
export function pushToolEvent(entryPoint: string, toolName: string, err: string, params: object): Promise<void>;
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* check for update
|
|
100
100
|
*/
|
|
101
|
-
export function checkForUpdate(component: string): void
|
|
101
|
+
export function checkForUpdate(component: string): Promise<void>;
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* The Skyramp YAML version constant.
|
package/src/utils.js
CHANGED
|
@@ -234,16 +234,27 @@ function iterate(fuzz_body) {
|
|
|
234
234
|
function pushToolEvent(entryPoint, toolName, err, params) {
|
|
235
235
|
const paramsStr = JSON.stringify(params);
|
|
236
236
|
|
|
237
|
-
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
pushToolEventWrapper.async(entryPoint, toolName, err, paramsStr, (err) => {
|
|
239
|
+
if (err) reject(err);
|
|
240
|
+
else resolve();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
/**
|
|
241
246
|
* check if new library is available
|
|
242
247
|
*
|
|
243
|
-
* @param {string} component -
|
|
248
|
+
* @param {string} component - Name of component
|
|
249
|
+
* @returns {Promise<void>}
|
|
244
250
|
*/
|
|
245
251
|
function checkForUpdate(component) {
|
|
246
|
-
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
checkForUpdateWrapper.async(component, (err) => {
|
|
254
|
+
if (err) reject(err);
|
|
255
|
+
else resolve();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
247
258
|
}
|
|
248
259
|
|
|
249
260
|
module.exports = {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Types (defined to match Zod schemas)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
// Note: These types match the runtime Zod schemas in workspace.js
|
|
8
|
+
// Changes to schemas should be reflected here
|
|
9
|
+
|
|
10
|
+
export interface ServiceApi {
|
|
11
|
+
schemaPath?: string;
|
|
12
|
+
authType?: "bearer" | "basic" | "oauth" | "apiKey" | "none";
|
|
13
|
+
authHeader?: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ServiceRuntimeDetails {
|
|
18
|
+
serverStartCommand: string;
|
|
19
|
+
runtime: "local" | "docker" | "k8s";
|
|
20
|
+
dockerNetwork?: string;
|
|
21
|
+
k8sNamespace?: string;
|
|
22
|
+
k8sContext?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Service {
|
|
26
|
+
serviceName: string;
|
|
27
|
+
language?: "python" | "typescript" | "javascript" | "java";
|
|
28
|
+
framework?: "playwright" | "pytest" | "robot" | "junit";
|
|
29
|
+
outputDir: string;
|
|
30
|
+
api?: ServiceApi;
|
|
31
|
+
runtimeDetails?: ServiceRuntimeDetails;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface WorkspaceSection {
|
|
35
|
+
repoName?: string;
|
|
36
|
+
repoUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface MetadataSection {
|
|
40
|
+
schemaVersion: string;
|
|
41
|
+
mcpVersion: string;
|
|
42
|
+
executorVersion: string;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkspaceConfig {
|
|
48
|
+
workspace?: WorkspaceSection;
|
|
49
|
+
metadata?: MetadataSection;
|
|
50
|
+
services?: Service[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Zod schemas
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export const serviceSchema: z.ZodType<Service>;
|
|
58
|
+
export const workspaceConfigSchema: z.ZodType<WorkspaceConfig>;
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Validation
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export function validateWorkspaceConfig(
|
|
65
|
+
config: unknown,
|
|
66
|
+
): z.SafeParseReturnType<WorkspaceConfig, WorkspaceConfig>;
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Helpers
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export function createDefaultConfig(): WorkspaceConfig;
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// WorkspaceConfigManager
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
export class WorkspaceConfigManager {
|
|
79
|
+
constructor(workspacePath: string);
|
|
80
|
+
|
|
81
|
+
/** Check if workspace config file exists */
|
|
82
|
+
exists(): Promise<boolean>;
|
|
83
|
+
|
|
84
|
+
/** Get the absolute path to the config file */
|
|
85
|
+
getConfigPath(): string;
|
|
86
|
+
|
|
87
|
+
/** Get the workspace root path */
|
|
88
|
+
getWorkspacePath(): string;
|
|
89
|
+
|
|
90
|
+
/** Read and parse workspace config */
|
|
91
|
+
read(): Promise<WorkspaceConfig>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Initialize workspace — creates .skyramp/workspace.yml with repo info.
|
|
95
|
+
* Auto-detects repoName and repoUrl from git when not provided.
|
|
96
|
+
*/
|
|
97
|
+
initialize(workspaceInfo?: WorkspaceSection): Promise<WorkspaceConfig>;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Update the metadata section of an existing workspace config.
|
|
101
|
+
* Only the provided fields are merged; metadata.updatedAt is refreshed
|
|
102
|
+
* automatically.
|
|
103
|
+
*/
|
|
104
|
+
updateMetadata(metadata: Partial<MetadataSection>): Promise<WorkspaceConfig>;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add a single service entry to the services array.
|
|
108
|
+
* If a service with the same serviceName already exists it is replaced
|
|
109
|
+
* (upsert semantics), otherwise the new entry is appended.
|
|
110
|
+
*/
|
|
111
|
+
addService(service: Service): Promise<WorkspaceConfig>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Constants
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export const WORKSPACE_DIR: string;
|
|
119
|
+
export const WORKSPACE_FILENAME: string;
|
package/src/workspace.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const yaml = require('js-yaml');
|
|
4
|
+
const { execFile } = require('child_process');
|
|
5
|
+
const { promisify } = require('util');
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const { z } = require('zod');
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const WORKSPACE_DIR = '.skyramp';
|
|
15
|
+
const WORKSPACE_FILENAME = 'workspace.yml';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Zod schemas
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const serviceSchema = z.object({
|
|
22
|
+
serviceName: z.string(),
|
|
23
|
+
language: z
|
|
24
|
+
.enum(['python', 'typescript', 'javascript', 'java'])
|
|
25
|
+
.optional(),
|
|
26
|
+
framework: z
|
|
27
|
+
.enum(['playwright', 'pytest', 'robot', 'junit'])
|
|
28
|
+
.optional(),
|
|
29
|
+
outputDir: z.string().optional(),
|
|
30
|
+
api: z
|
|
31
|
+
.object({
|
|
32
|
+
schemaPath: z.string().optional(),
|
|
33
|
+
authType: z
|
|
34
|
+
.enum(['bearer', 'basic', 'oauth', 'apiKey', 'none'])
|
|
35
|
+
.optional(),
|
|
36
|
+
authHeader: z.string().optional(),
|
|
37
|
+
baseUrl: z.string().optional(),
|
|
38
|
+
})
|
|
39
|
+
.strict()
|
|
40
|
+
.optional(),
|
|
41
|
+
runtimeDetails: z
|
|
42
|
+
.object({
|
|
43
|
+
serverStartCommand: z.string(),
|
|
44
|
+
runtime: z.enum(['local', 'docker', 'k8s']),
|
|
45
|
+
dockerNetwork: z.string().optional(),
|
|
46
|
+
k8sNamespace: z.string().optional(),
|
|
47
|
+
k8sContext: z.string().optional(),
|
|
48
|
+
})
|
|
49
|
+
.strict()
|
|
50
|
+
.optional(),
|
|
51
|
+
}).strict();
|
|
52
|
+
|
|
53
|
+
const workspaceConfigSchema = z.object({
|
|
54
|
+
workspace: z
|
|
55
|
+
.object({
|
|
56
|
+
repoName: z.string().optional(),
|
|
57
|
+
repoUrl: z.string().optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict()
|
|
60
|
+
.optional(),
|
|
61
|
+
metadata: z
|
|
62
|
+
.object({
|
|
63
|
+
schemaVersion: z.string(),
|
|
64
|
+
mcpVersion: z.string(),
|
|
65
|
+
executorVersion: z.string(),
|
|
66
|
+
createdAt: z.string(),
|
|
67
|
+
updatedAt: z.string(),
|
|
68
|
+
})
|
|
69
|
+
.strict()
|
|
70
|
+
.optional(),
|
|
71
|
+
services: z.array(serviceSchema).optional(),
|
|
72
|
+
}).strict();
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Validation helpers
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Throws a formatted Error from a failed Zod safe-parse result.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} label - Human-readable context (e.g. "Workspace validation").
|
|
82
|
+
* @param {import('zod').SafeParseError} zodError - The `.error` from safeParse.
|
|
83
|
+
*/
|
|
84
|
+
function throwValidationError(label, zodError) {
|
|
85
|
+
const messages = zodError.issues.map((i) => {
|
|
86
|
+
const pathLabel =
|
|
87
|
+
Array.isArray(i.path) && i.path.length > 0 ? i.path.join('.') : '<root>';
|
|
88
|
+
return `${pathLabel}: ${i.message}`;
|
|
89
|
+
});
|
|
90
|
+
throw new Error(`${label} failed:\n - ${messages.join('\n - ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validates a workspace configuration object against the Zod schema.
|
|
95
|
+
*
|
|
96
|
+
* @param {unknown} config - The configuration to validate.
|
|
97
|
+
* @returns {import('zod').SafeParseReturnType} Zod safe-parse result.
|
|
98
|
+
*/
|
|
99
|
+
function validateWorkspaceConfig(config) {
|
|
100
|
+
return workspaceConfigSchema.safeParse(config);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validates and returns the parsed data, or throws on failure.
|
|
105
|
+
*
|
|
106
|
+
* @param {unknown} config - The configuration to validate.
|
|
107
|
+
* @param {string} [label='Workspace validation'] - Error label.
|
|
108
|
+
* @returns {Object} The validated config data.
|
|
109
|
+
*/
|
|
110
|
+
function validateOrThrow(config, label = 'Workspace validation') {
|
|
111
|
+
const result = validateWorkspaceConfig(config);
|
|
112
|
+
if (!result.success) {
|
|
113
|
+
throwValidationError(label, result.error);
|
|
114
|
+
}
|
|
115
|
+
return result.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Defaults
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Creates a default workspace config with empty sections and timestamps.
|
|
124
|
+
*
|
|
125
|
+
* @returns {Object} Default workspace config.
|
|
126
|
+
*/
|
|
127
|
+
function createDefaultConfig() {
|
|
128
|
+
const now = new Date().toISOString();
|
|
129
|
+
return {
|
|
130
|
+
workspace: {},
|
|
131
|
+
metadata: {
|
|
132
|
+
schemaVersion: 'v1',
|
|
133
|
+
mcpVersion: '',
|
|
134
|
+
executorVersion: '',
|
|
135
|
+
createdAt: now,
|
|
136
|
+
updatedAt: now,
|
|
137
|
+
},
|
|
138
|
+
services: [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Low-level I/O helpers
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Writes a workspace config to disk as YAML.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} filePath - Absolute path to write to.
|
|
150
|
+
* @param {Object} config - The workspace config object.
|
|
151
|
+
* @returns {Promise<void>}
|
|
152
|
+
*/
|
|
153
|
+
async function writeWorkspaceFile(filePath, config) {
|
|
154
|
+
const header = '# Skyramp Workspace Configuration\n';
|
|
155
|
+
const body = yaml.dump(config, {
|
|
156
|
+
lineWidth: 120,
|
|
157
|
+
noRefs: true,
|
|
158
|
+
quotingType: '"',
|
|
159
|
+
forceQuotes: false,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await fs.writeFile(filePath, header + body, 'utf8');
|
|
164
|
+
} catch (err) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Failed to write workspace file to ${filePath}: ${err.message}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Auto-detects repo name and URL from git.
|
|
173
|
+
* Throws an error if the directory is not a git repository.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} [dirPath=process.cwd()]
|
|
176
|
+
* @param {boolean} [requireGit=true] - If true, throws error when not a git repo
|
|
177
|
+
* @returns {Promise<{ repoName: string|null, repoUrl: string|null }>}
|
|
178
|
+
*/
|
|
179
|
+
async function detectRepoInfo(dirPath, requireGit = true) {
|
|
180
|
+
const cwd = dirPath ? path.resolve(dirPath) : process.cwd();
|
|
181
|
+
let repoUrl = null;
|
|
182
|
+
let repoName = null;
|
|
183
|
+
let isGitRepo = false;
|
|
184
|
+
|
|
185
|
+
// First, check if this is a git repository
|
|
186
|
+
try {
|
|
187
|
+
await execFileAsync('git', ['rev-parse', '--git-dir'], {
|
|
188
|
+
cwd,
|
|
189
|
+
timeout: 5000,
|
|
190
|
+
});
|
|
191
|
+
isGitRepo = true;
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (requireGit) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`Workspace initialization requires a git repository. Directory '${cwd}' is not a git repository. ` +
|
|
196
|
+
`We cannot initialize the workspace in a non-git repository.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// If it's a git repo, try to get the remote URL
|
|
202
|
+
if (isGitRepo) {
|
|
203
|
+
try {
|
|
204
|
+
const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'origin'], {
|
|
205
|
+
cwd,
|
|
206
|
+
timeout: 5000,
|
|
207
|
+
});
|
|
208
|
+
repoUrl = stdout.trim();
|
|
209
|
+
} catch {
|
|
210
|
+
// Git repo exists but no remote configured - this is OK
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (repoUrl) {
|
|
214
|
+
const match = repoUrl.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
215
|
+
if (match) repoName = match[1];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!repoName) {
|
|
219
|
+
repoName = path.basename(cwd);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { repoName, repoUrl };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Low-level state management (standalone, path-based API)
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Loads and validates a workspace YAML file from disk.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} filePath - Absolute path to the workspace file.
|
|
234
|
+
* @returns {Promise<{ config: Object, filePath: string }>}
|
|
235
|
+
*/
|
|
236
|
+
async function loadWorkspaceFile(filePath) {
|
|
237
|
+
const resolvedPath = path.resolve(filePath);
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
await fs.access(resolvedPath);
|
|
241
|
+
} catch {
|
|
242
|
+
throw new Error(`Workspace file not found: ${resolvedPath}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const content = await fs.readFile(resolvedPath, 'utf8');
|
|
246
|
+
let rawConfig;
|
|
247
|
+
try {
|
|
248
|
+
// Use JSON_SCHEMA to prevent timestamp auto-conversion to Date objects
|
|
249
|
+
rawConfig = yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
250
|
+
} catch (err) {
|
|
251
|
+
throw new Error(`Failed to parse workspace YAML: ${err.message}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const validated = validateOrThrow(rawConfig);
|
|
255
|
+
return { config: validated, filePath: resolvedPath };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// WorkspaceConfigManager
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* High-level manager for .skyramp/workspace.yml.
|
|
264
|
+
*
|
|
265
|
+
* Provides three focused mutation methods:
|
|
266
|
+
* 1. initialize() — create workspace file with repo info
|
|
267
|
+
* 2. updateMetadata() — update metadata section
|
|
268
|
+
* 3. addService() — upsert a single service entry
|
|
269
|
+
*
|
|
270
|
+
* @param {string} workspacePath - Repository / project root.
|
|
271
|
+
*/
|
|
272
|
+
class WorkspaceConfigManager {
|
|
273
|
+
constructor(workspacePath) {
|
|
274
|
+
this.workspacePath = path.resolve(workspacePath);
|
|
275
|
+
this.skyrampDir = path.join(this.workspacePath, WORKSPACE_DIR);
|
|
276
|
+
this.configPath = path.join(this.skyrampDir, WORKSPACE_FILENAME);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Check if workspace config file exists */
|
|
280
|
+
async exists() {
|
|
281
|
+
try {
|
|
282
|
+
await fs.access(this.configPath);
|
|
283
|
+
return true;
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Get the absolute path to the config file */
|
|
290
|
+
getConfigPath() {
|
|
291
|
+
return this.configPath;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Get the workspace root path */
|
|
295
|
+
getWorkspacePath() {
|
|
296
|
+
return this.workspacePath;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Read and parse workspace config from disk.
|
|
301
|
+
*
|
|
302
|
+
* @returns {Promise<Object>} The validated WorkspaceConfig.
|
|
303
|
+
*/
|
|
304
|
+
async read() {
|
|
305
|
+
if (!(await this.exists())) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Workspace config not found at ${this.configPath}. Initialize the workspace first.`,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const result = await loadWorkspaceFile(this.configPath);
|
|
311
|
+
return result.config;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// -----------------------------------------------------------------------
|
|
315
|
+
// 1. Initialize workspace with repo info
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Initialize workspace — creates .skyramp/workspace.yml with repo info.
|
|
320
|
+
* Auto-detects repoName and repoUrl from git when not provided.
|
|
321
|
+
* Produces an empty services array and default metadata timestamps.
|
|
322
|
+
*
|
|
323
|
+
* @param {Object} [workspaceInfo] - Optional explicit repo info.
|
|
324
|
+
* @param {string} [workspaceInfo.repoName] - Repository name.
|
|
325
|
+
* @param {string} [workspaceInfo.repoUrl] - Repository URL.
|
|
326
|
+
* @returns {Promise<Object>} The validated WorkspaceConfig.
|
|
327
|
+
*/
|
|
328
|
+
async initialize(workspaceInfo = {}) {
|
|
329
|
+
await fs.mkdir(this.skyrampDir, { recursive: true });
|
|
330
|
+
|
|
331
|
+
// Auto-detect repo info from workspace root when not explicitly provided
|
|
332
|
+
const detected = await detectRepoInfo(this.workspacePath);
|
|
333
|
+
|
|
334
|
+
const config = createDefaultConfig();
|
|
335
|
+
config.workspace = {
|
|
336
|
+
repoName: workspaceInfo.repoName || detected.repoName || undefined,
|
|
337
|
+
repoUrl: workspaceInfo.repoUrl || detected.repoUrl || undefined,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const validated = validateOrThrow(config);
|
|
341
|
+
await writeWorkspaceFile(this.configPath, validated);
|
|
342
|
+
return validated;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// -----------------------------------------------------------------------
|
|
346
|
+
// 2. Update metadata
|
|
347
|
+
// -----------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Update the metadata section of an existing workspace config.
|
|
351
|
+
* Only the provided fields are merged; others are left untouched.
|
|
352
|
+
* metadata.updatedAt is refreshed automatically.
|
|
353
|
+
*
|
|
354
|
+
* @param {Object} metadata - Partial metadata fields to merge.
|
|
355
|
+
* @param {string} [metadata.schemaVersion]
|
|
356
|
+
* @param {string} [metadata.mcpVersion]
|
|
357
|
+
* @param {string} [metadata.executorVersion]
|
|
358
|
+
* @returns {Promise<Object>} The updated, validated WorkspaceConfig.
|
|
359
|
+
*/
|
|
360
|
+
async updateMetadata(metadata) {
|
|
361
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
362
|
+
throw new Error('metadata must be a non-null object');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const config = await this.read();
|
|
366
|
+
const existingMetadata = config.metadata || createDefaultConfig().metadata;
|
|
367
|
+
|
|
368
|
+
config.metadata = {
|
|
369
|
+
...existingMetadata,
|
|
370
|
+
...metadata,
|
|
371
|
+
createdAt: existingMetadata.createdAt, // Preserve original createdAt
|
|
372
|
+
updatedAt: new Date().toISOString(),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const validated = validateOrThrow(config);
|
|
376
|
+
await writeWorkspaceFile(this.configPath, validated);
|
|
377
|
+
return validated;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// -----------------------------------------------------------------------
|
|
381
|
+
// 3. Add / upsert a single service
|
|
382
|
+
// -----------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Add a single service entry to the services array.
|
|
386
|
+
* If a service with the same serviceName already exists it is replaced
|
|
387
|
+
* (upsert semantics), otherwise the new entry is appended.
|
|
388
|
+
* metadata.updatedAt is refreshed automatically.
|
|
389
|
+
*
|
|
390
|
+
* @param {Object} service - A service object matching the serviceSchema.
|
|
391
|
+
* @param {string} service.serviceName - Unique service identifier (required).
|
|
392
|
+
* @returns {Promise<Object>} The updated, validated WorkspaceConfig.
|
|
393
|
+
*/
|
|
394
|
+
async addService(service) {
|
|
395
|
+
if (!service || typeof service !== 'object') {
|
|
396
|
+
throw new Error('service must be a non-null object');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Validate the individual service entry first
|
|
400
|
+
const svcResult = serviceSchema.safeParse(service);
|
|
401
|
+
if (!svcResult.success) {
|
|
402
|
+
throwValidationError('Service validation', svcResult.error);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const config = await this.read();
|
|
406
|
+
const services = config.services || [];
|
|
407
|
+
|
|
408
|
+
// Upsert: replace existing entry with same serviceName, or append
|
|
409
|
+
const idx = services.findIndex(
|
|
410
|
+
(s) => s.serviceName === svcResult.data.serviceName,
|
|
411
|
+
);
|
|
412
|
+
if (idx >= 0) {
|
|
413
|
+
services[idx] = svcResult.data;
|
|
414
|
+
} else {
|
|
415
|
+
services.push(svcResult.data);
|
|
416
|
+
}
|
|
417
|
+
config.services = services;
|
|
418
|
+
|
|
419
|
+
// Refresh timestamp
|
|
420
|
+
if (!config.metadata) {
|
|
421
|
+
config.metadata = createDefaultConfig().metadata;
|
|
422
|
+
}
|
|
423
|
+
config.metadata.updatedAt = new Date().toISOString();
|
|
424
|
+
|
|
425
|
+
const validated = validateOrThrow(config);
|
|
426
|
+
await writeWorkspaceFile(this.configPath, validated);
|
|
427
|
+
return validated;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ---------------------------------------------------------------------------
|
|
432
|
+
// Exports
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
module.exports = {
|
|
436
|
+
// Schema
|
|
437
|
+
workspaceConfigSchema,
|
|
438
|
+
serviceSchema,
|
|
439
|
+
// High-level manager
|
|
440
|
+
WorkspaceConfigManager,
|
|
441
|
+
// Validation
|
|
442
|
+
validateWorkspaceConfig,
|
|
443
|
+
// Helpers
|
|
444
|
+
createDefaultConfig,
|
|
445
|
+
// Constants
|
|
446
|
+
WORKSPACE_DIR,
|
|
447
|
+
WORKSPACE_FILENAME,
|
|
448
|
+
};
|