donobu 5.23.0 → 5.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/apis/SuitesApi.d.ts +36 -0
  2. package/dist/apis/SuitesApi.js +68 -0
  3. package/dist/apis/TestsApi.d.ts +40 -0
  4. package/dist/apis/TestsApi.js +86 -0
  5. package/dist/assets/openapi-schema.yaml +15 -0
  6. package/dist/cli/install-donobu-plugin.d.ts +19 -0
  7. package/dist/cli/install-donobu-plugin.js +286 -22
  8. package/dist/esm/apis/SuitesApi.d.ts +36 -0
  9. package/dist/esm/apis/SuitesApi.js +68 -0
  10. package/dist/esm/apis/TestsApi.d.ts +40 -0
  11. package/dist/esm/apis/TestsApi.js +86 -0
  12. package/dist/esm/assets/openapi-schema.yaml +15 -0
  13. package/dist/esm/cli/install-donobu-plugin.d.ts +19 -0
  14. package/dist/esm/cli/install-donobu-plugin.js +286 -22
  15. package/dist/esm/main.d.ts +1 -0
  16. package/dist/esm/managers/AdminApiController.d.ts +8 -0
  17. package/dist/esm/managers/AdminApiController.js +29 -0
  18. package/dist/esm/managers/DonobuFlowsManager.d.ts +29 -1
  19. package/dist/esm/managers/DonobuFlowsManager.js +74 -28
  20. package/dist/esm/managers/DonobuStack.js +1 -1
  21. package/dist/esm/managers/FederatedPagination.js +10 -1
  22. package/dist/esm/managers/FlowCatalog.js +31 -39
  23. package/dist/esm/managers/FlowDependencyAnalyzer.js +12 -0
  24. package/dist/esm/managers/PluginLoader.d.ts +33 -7
  25. package/dist/esm/managers/PluginLoader.js +102 -37
  26. package/dist/esm/managers/SuitesManager.js +23 -6
  27. package/dist/esm/managers/TestsManager.d.ts +24 -3
  28. package/dist/esm/managers/TestsManager.js +123 -28
  29. package/dist/esm/models/BrowserConfig.d.ts +3 -0
  30. package/dist/esm/models/BrowserStateFlowReference.d.ts +3 -0
  31. package/dist/esm/models/BrowserStateFlowReference.js +13 -2
  32. package/dist/esm/models/CreateDonobuFlow.d.ts +4 -0
  33. package/dist/esm/models/CreateDonobuFlow.js +4 -0
  34. package/dist/esm/models/CreateSuite.d.ts +3 -0
  35. package/dist/esm/models/CreateTest.d.ts +3 -0
  36. package/dist/esm/models/FlowMetadata.d.ts +4 -0
  37. package/dist/esm/models/FlowMetadata.js +8 -0
  38. package/dist/esm/models/RunConfig.d.ts +6 -0
  39. package/dist/esm/models/SuiteMetadata.d.ts +3 -0
  40. package/dist/esm/models/TargetConfig.d.ts +3 -0
  41. package/dist/esm/models/TestMetadata.d.ts +3 -0
  42. package/dist/esm/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
  43. package/dist/esm/persistence/flows/FlowsPersistenceSqlite.js +11 -0
  44. package/dist/esm/persistence/flows/FlowsPersistenceVolatile.js +7 -1
  45. package/dist/esm/tools/AssertPageTool.d.ts +2 -2
  46. package/dist/esm/tools/TriggerDonobuFlowTool.d.ts +8 -0
  47. package/dist/esm/utils/MiscUtils.d.ts +13 -0
  48. package/dist/esm/utils/MiscUtils.js +21 -0
  49. package/dist/main.d.ts +1 -0
  50. package/dist/managers/AdminApiController.d.ts +8 -0
  51. package/dist/managers/AdminApiController.js +29 -0
  52. package/dist/managers/DonobuFlowsManager.d.ts +29 -1
  53. package/dist/managers/DonobuFlowsManager.js +74 -28
  54. package/dist/managers/DonobuStack.js +1 -1
  55. package/dist/managers/FederatedPagination.js +10 -1
  56. package/dist/managers/FlowCatalog.js +31 -39
  57. package/dist/managers/FlowDependencyAnalyzer.js +12 -0
  58. package/dist/managers/PluginLoader.d.ts +33 -7
  59. package/dist/managers/PluginLoader.js +102 -37
  60. package/dist/managers/SuitesManager.js +23 -6
  61. package/dist/managers/TestsManager.d.ts +24 -3
  62. package/dist/managers/TestsManager.js +123 -28
  63. package/dist/models/BrowserConfig.d.ts +3 -0
  64. package/dist/models/BrowserStateFlowReference.d.ts +3 -0
  65. package/dist/models/BrowserStateFlowReference.js +13 -2
  66. package/dist/models/CreateDonobuFlow.d.ts +4 -0
  67. package/dist/models/CreateDonobuFlow.js +4 -0
  68. package/dist/models/CreateSuite.d.ts +3 -0
  69. package/dist/models/CreateTest.d.ts +3 -0
  70. package/dist/models/FlowMetadata.d.ts +4 -0
  71. package/dist/models/FlowMetadata.js +8 -0
  72. package/dist/models/RunConfig.d.ts +6 -0
  73. package/dist/models/SuiteMetadata.d.ts +3 -0
  74. package/dist/models/TargetConfig.d.ts +3 -0
  75. package/dist/models/TestMetadata.d.ts +3 -0
  76. package/dist/persistence/flows/FlowsPersistenceDonobuApi.js +7 -0
  77. package/dist/persistence/flows/FlowsPersistenceSqlite.js +11 -0
  78. package/dist/persistence/flows/FlowsPersistenceVolatile.js +7 -1
  79. package/dist/tools/AssertPageTool.d.ts +2 -2
  80. package/dist/tools/TriggerDonobuFlowTool.d.ts +8 -0
  81. package/dist/utils/MiscUtils.d.ts +13 -0
  82. package/dist/utils/MiscUtils.js +21 -0
  83. package/package.json +1 -1
@@ -0,0 +1,36 @@
1
+ import type { Request, Response } from 'express';
2
+ import type { SuitesManager } from '../managers/SuitesManager';
3
+ import type { TestsManager } from '../managers/TestsManager';
4
+ /**
5
+ * API controller for managing suites.
6
+ */
7
+ export declare class SuitesApi {
8
+ private readonly suitesManager;
9
+ private readonly testsManager;
10
+ constructor(suitesManager: SuitesManager, testsManager: TestsManager);
11
+ /**
12
+ * GET /api/suites — list suites with optional filtering and pagination.
13
+ */
14
+ getSuites(req: Request, res: Response): Promise<void>;
15
+ /**
16
+ * POST /api/suites — create a new suite.
17
+ */
18
+ createSuite(req: Request, res: Response): Promise<void>;
19
+ /**
20
+ * GET /api/suites/:suiteId — get a suite by ID.
21
+ */
22
+ getSuite(req: Request, res: Response): Promise<void>;
23
+ /**
24
+ * PUT /api/suites/:suiteId — update an existing suite.
25
+ */
26
+ updateSuite(req: Request, res: Response): Promise<void>;
27
+ /**
28
+ * DELETE /api/suites/:suiteId — delete a suite (tests are orphaned).
29
+ */
30
+ deleteSuite(req: Request, res: Response): Promise<void>;
31
+ /**
32
+ * GET /api/suites/:suiteId/tests — list tests in a suite.
33
+ */
34
+ getSuiteTests(req: Request, res: Response): Promise<void>;
35
+ }
36
+ //# sourceMappingURL=SuitesApi.d.ts.map
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SuitesApi = void 0;
4
+ const CreateSuite_1 = require("../models/CreateSuite");
5
+ const SuiteMetadata_1 = require("../models/SuiteMetadata");
6
+ /**
7
+ * API controller for managing suites.
8
+ */
9
+ class SuitesApi {
10
+ constructor(suitesManager, testsManager) {
11
+ this.suitesManager = suitesManager;
12
+ this.testsManager = testsManager;
13
+ }
14
+ /**
15
+ * GET /api/suites — list suites with optional filtering and pagination.
16
+ */
17
+ async getSuites(req, res) {
18
+ const query = SuiteMetadata_1.SuitesQuerySchema.parse(req.query);
19
+ const result = await this.suitesManager.getSuites(query);
20
+ res.json({ suites: result.items, nextPageToken: result.nextPageToken });
21
+ }
22
+ /**
23
+ * POST /api/suites — create a new suite.
24
+ */
25
+ async createSuite(req, res) {
26
+ const params = CreateSuite_1.CreateSuiteSchema.parse(req.body);
27
+ const suite = await this.suitesManager.createSuite(params);
28
+ res.json(suite);
29
+ }
30
+ /**
31
+ * GET /api/suites/:suiteId — get a suite by ID.
32
+ */
33
+ async getSuite(req, res) {
34
+ const suiteId = String(req.params.suiteId);
35
+ const suite = await this.suitesManager.getSuiteById(suiteId);
36
+ res.json(suite);
37
+ }
38
+ /**
39
+ * PUT /api/suites/:suiteId — update an existing suite.
40
+ */
41
+ async updateSuite(req, res) {
42
+ const suiteId = String(req.params.suiteId);
43
+ const suiteMetadata = SuiteMetadata_1.SuiteMetadataSchema.parse({
44
+ ...req.body,
45
+ id: suiteId,
46
+ });
47
+ await this.suitesManager.updateSuite(suiteMetadata);
48
+ res.json(suiteMetadata);
49
+ }
50
+ /**
51
+ * DELETE /api/suites/:suiteId — delete a suite (tests are orphaned).
52
+ */
53
+ async deleteSuite(req, res) {
54
+ const suiteId = String(req.params.suiteId);
55
+ await this.suitesManager.deleteSuite(suiteId);
56
+ res.status(200).json({ deleted: true });
57
+ }
58
+ /**
59
+ * GET /api/suites/:suiteId/tests — list tests in a suite.
60
+ */
61
+ async getSuiteTests(req, res) {
62
+ const suiteId = String(req.params.suiteId);
63
+ const result = await this.testsManager.getTests({ suiteId });
64
+ res.json({ tests: result.items, nextPageToken: result.nextPageToken });
65
+ }
66
+ }
67
+ exports.SuitesApi = SuitesApi;
68
+ //# sourceMappingURL=SuitesApi.js.map
@@ -0,0 +1,40 @@
1
+ import type { Request, Response } from 'express';
2
+ import type { DonobuFlowsManager } from '../managers/DonobuFlowsManager';
3
+ import type { TestsManager } from '../managers/TestsManager';
4
+ /**
5
+ * API controller for managing tests.
6
+ */
7
+ export declare class TestsApi {
8
+ private readonly testsManager;
9
+ private readonly flowsManager;
10
+ constructor(testsManager: TestsManager, flowsManager: DonobuFlowsManager);
11
+ /**
12
+ * GET /api/tests — list tests with optional filtering and pagination.
13
+ */
14
+ getTests(req: Request, res: Response): Promise<void>;
15
+ /**
16
+ * POST /api/tests — create a new test.
17
+ */
18
+ createTest(req: Request, res: Response): Promise<void>;
19
+ /**
20
+ * GET /api/tests/:testId — get a test by ID.
21
+ */
22
+ getTest(req: Request, res: Response): Promise<void>;
23
+ /**
24
+ * PUT /api/tests/:testId — update an existing test.
25
+ */
26
+ updateTest(req: Request, res: Response): Promise<void>;
27
+ /**
28
+ * DELETE /api/tests/:testId — delete a test and all its flows.
29
+ */
30
+ deleteTest(req: Request, res: Response): Promise<void>;
31
+ /**
32
+ * GET /api/tests/:testId/flows — list flows for a test.
33
+ */
34
+ getTestFlows(req: Request, res: Response): Promise<void>;
35
+ /**
36
+ * POST /api/tests/:testId/run — execute a test (create a new flow from it).
37
+ */
38
+ runTest(req: Request, res: Response): Promise<void>;
39
+ }
40
+ //# sourceMappingURL=TestsApi.d.ts.map
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestsApi = void 0;
4
+ const CreateTest_1 = require("../models/CreateTest");
5
+ const TestMetadata_1 = require("../models/TestMetadata");
6
+ /**
7
+ * API controller for managing tests.
8
+ */
9
+ class TestsApi {
10
+ constructor(testsManager, flowsManager) {
11
+ this.testsManager = testsManager;
12
+ this.flowsManager = flowsManager;
13
+ }
14
+ /**
15
+ * GET /api/tests — list tests with optional filtering and pagination.
16
+ */
17
+ async getTests(req, res) {
18
+ const query = TestMetadata_1.TestsQuerySchema.parse(req.query);
19
+ const result = await this.testsManager.getTests(query);
20
+ res.json({ tests: result.items, nextPageToken: result.nextPageToken });
21
+ }
22
+ /**
23
+ * POST /api/tests — create a new test.
24
+ */
25
+ async createTest(req, res) {
26
+ const params = CreateTest_1.CreateTestSchema.parse(req.body);
27
+ const test = await this.testsManager.createTest(params);
28
+ res.json(test);
29
+ }
30
+ /**
31
+ * GET /api/tests/:testId — get a test by ID.
32
+ */
33
+ async getTest(req, res) {
34
+ const testId = String(req.params.testId);
35
+ const test = await this.testsManager.getTestById(testId);
36
+ res.json(test);
37
+ }
38
+ /**
39
+ * PUT /api/tests/:testId — update an existing test.
40
+ */
41
+ async updateTest(req, res) {
42
+ const testId = String(req.params.testId);
43
+ const testMetadata = TestMetadata_1.TestMetadataSchema.parse({
44
+ ...req.body,
45
+ id: testId,
46
+ });
47
+ await this.testsManager.updateTest(testMetadata);
48
+ res.json(testMetadata);
49
+ }
50
+ /**
51
+ * DELETE /api/tests/:testId — delete a test and all its flows.
52
+ */
53
+ async deleteTest(req, res) {
54
+ const testId = String(req.params.testId);
55
+ await this.testsManager.deleteTest(testId);
56
+ res.status(200).json({ deleted: true });
57
+ }
58
+ /**
59
+ * GET /api/tests/:testId/flows — list flows for a test.
60
+ */
61
+ async getTestFlows(req, res) {
62
+ const testId = String(req.params.testId);
63
+ const flows = await this.flowsManager.getFlows({ testId });
64
+ res.json({ flows: flows.items, nextPageToken: flows.nextPageToken });
65
+ }
66
+ /**
67
+ * POST /api/tests/:testId/run — execute a test (create a new flow from it).
68
+ */
69
+ async runTest(req, res) {
70
+ const testId = String(req.params.testId);
71
+ const newFlowConfig = await this.testsManager.getNewFlowFromTest(testId);
72
+ const flowHandle = await this.flowsManager.createFlow(newFlowConfig);
73
+ // After starting an autonomous run, switch the test to deterministic
74
+ // so subsequent runs replay the recorded actions.
75
+ if (newFlowConfig.initialRunMode === 'AUTONOMOUS') {
76
+ const test = await this.testsManager.getTestById(testId);
77
+ await this.testsManager.updateTest({
78
+ ...test,
79
+ nextRunMode: 'DETERMINISTIC',
80
+ });
81
+ }
82
+ res.json(flowHandle.donobuFlow.metadata);
83
+ }
84
+ }
85
+ exports.TestsApi = TestsApi;
86
+ //# sourceMappingURL=TestsApi.js.map
@@ -1496,6 +1496,7 @@ components:
1496
1496
  to restore when starting a flow. Exactly one variant should be provided:
1497
1497
  - `id` — Reference a flow by its ID and restore its persisted browser state.
1498
1498
  - `name` — Reference a flow by its name and restore its persisted browser state.
1499
+ - `testId` — Reference a test by its ID and restore browser state from the most recent successful flow belonging to that test.
1499
1500
  - `json` — Provide the browser state inline as a JSON object.
1500
1501
  oneOf:
1501
1502
  - type: 'object'
@@ -1526,6 +1527,20 @@ components:
1526
1527
  value:
1527
1528
  type: 'string'
1528
1529
  description: 'The name of the flow whose browser state should be restored.'
1530
+ - type: 'object'
1531
+ description: 'Reference a test by ID to restore browser state from its most recent successful flow.'
1532
+ required:
1533
+ - 'type'
1534
+ - 'value'
1535
+ properties:
1536
+ type:
1537
+ type: 'string'
1538
+ enum:
1539
+ - 'testId'
1540
+ description: 'Indicates the value is a test ID.'
1541
+ value:
1542
+ type: 'string'
1543
+ description: 'A test ID. The browser state will be restored from the most recent successful flow belonging to this test.'
1529
1544
  - type: 'object'
1530
1545
  description: 'Provide browser state inline as a JSON object.'
1531
1546
  required:
@@ -2,5 +2,24 @@
2
2
  export declare function getPluginName(): Promise<string>;
3
3
  export declare function buildPlugin(): Promise<void>;
4
4
  export declare function installPlugin(): Promise<void>;
5
+ type ReleaseSpec = {
6
+ /** Plugin name as it appears on the tarball asset and as the install dir. */
7
+ name: string;
8
+ /** Pinned version, or `undefined` to resolve the latest. */
9
+ version: string | undefined;
10
+ };
11
+ /**
12
+ * Parses `name`, `name@version`, `@scope/name`, or `@scope/name@version`.
13
+ * The last `@` after the first character (if any) separates name and version.
14
+ */
15
+ export declare function parseReleaseSpec(spec: string): ReleaseSpec;
16
+ export declare function installFromRelease(spec: ReleaseSpec, apiKey: string, into: string | undefined): Promise<void>;
17
+ type ParsedArgs = {
18
+ fromRelease: string | undefined;
19
+ apiKey: string | undefined;
20
+ into: string | undefined;
21
+ };
22
+ export declare function parseArgs(argv: string[]): ParsedArgs;
5
23
  export declare function main(): Promise<void>;
24
+ export {};
6
25
  //# sourceMappingURL=install-donobu-plugin.d.ts.map
@@ -4,46 +4,57 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.getPluginName = getPluginName;
5
5
  exports.buildPlugin = buildPlugin;
6
6
  exports.installPlugin = installPlugin;
7
+ exports.parseReleaseSpec = parseReleaseSpec;
8
+ exports.installFromRelease = installFromRelease;
9
+ exports.parseArgs = parseArgs;
7
10
  exports.main = main;
8
11
  /**
9
12
  * @fileoverview Donobu Plugin Installation CLI
10
13
  *
11
- * A command-line utility for building and installing Donobu plugins into the local Donobu Studio environment.
12
- * This script automates the process of compiling a plugin and copying it to the appropriate plugins directory
13
- * where Donobu Studio can discover and load it.
14
+ * A command-line utility for installing Donobu plugins into the local Donobu
15
+ * Studio environment. Two modes:
14
16
  *
15
- * @usage
16
- * Run this command from within a Donobu plugin directory (must contain package.json and src/ directory):
17
+ * 1. **Local build-and-copy** (default): run from a plugin source directory
18
+ * with `package.json` and `src/`. Builds via `npm run build` and copies
19
+ * `dist/` into Donobu's plugins directory.
20
+ *
21
+ * 2. **Release fetch** (`--from-release`): downloads a pre-built first-party
22
+ * plugin bundle from the Donobu API (which checks the customer's
23
+ * account entitlement before serving bytes from the private release
24
+ * registry), verifies the sha256, and extracts it into Donobu's plugins
25
+ * directory. No local project required.
17
26
  *
27
+ * @usage
18
28
  * ```bash
29
+ * # Local mode
19
30
  * npm exec install-donobu-plugin
20
- * ```
21
31
  *
22
- * @process
23
- * 1. Validates that the current directory is a valid plugin directory.
24
- * 2. Determines the plugin name from package.json, git repository, or username.
25
- * 3. Runs `npm run build` to compile the plugin.
26
- * 4. Copies the built plugin to the Donobu plugin directory (OS-variant).
27
- * 5. Provides instructions to restart Donobu Studio to see the new tools.
32
+ * # Release mode (latest)
33
+ * DONOBU_API_KEY=DB_… npx install-donobu-plugin \
34
+ * --from-release @donobu/donobu-mobile
28
35
  *
29
- * @requirements
30
- * - Must be run from a directory containing package.json.
31
- * - Must be run from a directory containing src/ directory.
32
- * - The project must have a working `npm run build` script.
33
- * - Write permissions to the Donobu Studio plugins directory.
34
- *
35
- * @output
36
- * - Plugin compiled and installed to Donobu Studio plugins directory.
37
- * - Console output with installation progress and final location.
36
+ * # Release mode (pinned version)
37
+ * DONOBU_API_KEY=DB_… npx install-donobu-plugin \
38
+ * --from-release @donobu/donobu-mobile@1.0.0
39
+ * ```
38
40
  */
39
41
  const child_process_1 = require("child_process");
42
+ const crypto_1 = require("crypto");
40
43
  const fs_1 = require("fs");
41
44
  const promises_1 = require("fs/promises");
45
+ const os_1 = require("os");
42
46
  const path_1 = require("path");
47
+ const promises_2 = require("stream/promises");
43
48
  const util_1 = require("util");
44
49
  const Logger_1 = require("../utils/Logger");
45
50
  const MiscUtils_1 = require("../utils/MiscUtils");
46
51
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
52
+ /**
53
+ * Default Donobu API base URL. Matches the default in `envVars.ts` so that a
54
+ * customer with a production `DONOBU_API_KEY` needs no extra configuration
55
+ * to install plugins. Override for staging via `DONOBU_API_BASE_URL`.
56
+ */
57
+ const DEFAULT_DONOBU_API_BASE_URL = 'https://donobu-prd-api-service-73193699649.us-central1.run.app';
47
58
  async function getPluginName() {
48
59
  try {
49
60
  // First, try to read the plugin name from package.json
@@ -95,9 +106,262 @@ async function installPlugin() {
95
106
  Logger_1.appLogger.info(`Plugin installed successfully to: ${thisPluginDir}`);
96
107
  Logger_1.appLogger.info('Restart Donobu to load the plugin.');
97
108
  }
109
+ /**
110
+ * Parses `name`, `name@version`, `@scope/name`, or `@scope/name@version`.
111
+ * The last `@` after the first character (if any) separates name and version.
112
+ */
113
+ function parseReleaseSpec(spec) {
114
+ if (!spec) {
115
+ throw new Error('--from-release requires a plugin name');
116
+ }
117
+ const atIdx = spec.lastIndexOf('@');
118
+ if (atIdx > 0) {
119
+ return { name: spec.slice(0, atIdx), version: spec.slice(atIdx + 1) };
120
+ }
121
+ return { name: spec, version: undefined };
122
+ }
123
+ function getDonobuApiBaseUrl() {
124
+ return (process.env.DONOBU_API_BASE_URL?.replace(/\/$/, '') ??
125
+ DEFAULT_DONOBU_API_BASE_URL);
126
+ }
127
+ function getDonobuApiKey(explicit) {
128
+ const key = explicit ?? process.env.DONOBU_API_KEY;
129
+ if (!key) {
130
+ throw new Error('Missing DONOBU_API_KEY. Pass --api-key <key>, set DONOBU_API_KEY in ' +
131
+ 'your environment, or retrieve a key at https://donobu.com/account/keys.');
132
+ }
133
+ return key;
134
+ }
135
+ /**
136
+ * Surface donobu-api JSON error bodies (`{ error, message }`) in the CLI's
137
+ * error output, so customers see the server's description instead of a bare
138
+ * HTTP status code.
139
+ */
140
+ async function extractApiErrorMessage(res) {
141
+ try {
142
+ const body = (await res.clone().json());
143
+ if (body.message) {
144
+ return body.message;
145
+ }
146
+ if (body.error) {
147
+ return body.error;
148
+ }
149
+ }
150
+ catch {
151
+ /* fall through */
152
+ }
153
+ return `${res.status} ${res.statusText}`;
154
+ }
155
+ async function fetchReleaseMetadata(baseUrl, apiKey, spec) {
156
+ const params = new URLSearchParams({
157
+ name: spec.name,
158
+ version: spec.version ?? 'latest',
159
+ });
160
+ const url = `${baseUrl}/v1/plugins/release?${params.toString()}`;
161
+ const res = await fetch(url, {
162
+ headers: {
163
+ Accept: 'application/json',
164
+ Authorization: `Bearer ${apiKey}`,
165
+ 'User-Agent': 'install-donobu-plugin',
166
+ },
167
+ });
168
+ if (!res.ok) {
169
+ throw new Error(`Failed to resolve ${spec.name}@${spec.version ?? 'latest'}: ` +
170
+ (await extractApiErrorMessage(res)));
171
+ }
172
+ return (await res.json());
173
+ }
174
+ async function downloadTarball(baseUrl, apiKey, name, version, destPath) {
175
+ const params = new URLSearchParams({ name, version });
176
+ const url = `${baseUrl}/v1/plugins/release/tarball?${params.toString()}`;
177
+ const res = await fetch(url, {
178
+ headers: {
179
+ Accept: 'application/octet-stream',
180
+ Authorization: `Bearer ${apiKey}`,
181
+ 'User-Agent': 'install-donobu-plugin',
182
+ },
183
+ });
184
+ if (!res.ok || !res.body) {
185
+ throw new Error(`Failed to download ${name}@${version}: ` +
186
+ (await extractApiErrorMessage(res)));
187
+ }
188
+ await (0, promises_2.pipeline)(res.body, (0, fs_1.createWriteStream)(destPath));
189
+ }
190
+ async function fetchEntitlementsEnvelope(baseUrl, apiKey) {
191
+ const res = await fetch(`${baseUrl}/v1/entitlements`, {
192
+ headers: {
193
+ Accept: 'application/json',
194
+ Authorization: `Bearer ${apiKey}`,
195
+ 'User-Agent': 'install-donobu-plugin',
196
+ },
197
+ });
198
+ if (!res.ok) {
199
+ throw new Error(`Failed to fetch entitlements: ` + (await extractApiErrorMessage(res)));
200
+ }
201
+ // Persist the exact JSON bytes the server sent. Reserializing would risk
202
+ // reordering keys in a way that invalidates the signature downstream.
203
+ const raw = await res.text();
204
+ return { raw, envelope: JSON.parse(raw) };
205
+ }
206
+ async function sha256OfFile(path) {
207
+ const hash = (0, crypto_1.createHash)('sha256');
208
+ const buf = await (0, promises_1.readFile)(path);
209
+ hash.update(buf);
210
+ return hash.digest('hex');
211
+ }
212
+ async function extractTarball(tarballPath, destDir) {
213
+ await (0, promises_1.mkdir)(destDir, { recursive: true });
214
+ // `tar` handles both gzip and xz transparently with `-xf`. `-C` sets the
215
+ // extraction root. Expect the tarball to contain the flat plugin payload
216
+ // (index.mjs, package.json, etc.) at its root — not nested under dist/.
217
+ await execAsync(`tar -xf "${tarballPath}" -C "${destDir}"`);
218
+ }
219
+ /**
220
+ * Filename for the signed entitlement envelope when colocated with a
221
+ * plugin's extracted bundle. Matches the constant in the plugin's
222
+ * entitlement-check code; changing one without the other breaks license
223
+ * discovery.
224
+ */
225
+ const LICENSE_FILENAME = '.donobu-license.json';
226
+ /**
227
+ * Resolve the parent directory into which the plugin's own dir will be
228
+ * placed. Without `--into`, we use the default `<app-data>/plugins/`
229
+ * location the Donobu Studio plugin loader scans. With `--into`, we
230
+ * honor the caller's path (useful for code-based test setups that want
231
+ * the plugin discoverable inside their own `node_modules/@donobu/`).
232
+ */
233
+ function resolveInstallParentDir(into) {
234
+ if (into) {
235
+ return (0, path_1.resolve)(into);
236
+ }
237
+ return (0, path_1.join)(MiscUtils_1.MiscUtils.baseWorkingDirectory(), 'plugins');
238
+ }
239
+ async function installFromRelease(spec, apiKey, into) {
240
+ const baseUrl = getDonobuApiBaseUrl();
241
+ Logger_1.appLogger.info(`Resolving ${spec.name}@${spec.version ?? 'latest'} from ${baseUrl}...`);
242
+ const { name, version, manifest } = await fetchReleaseMetadata(baseUrl, apiKey, spec);
243
+ if (manifest.name !== name || manifest.version !== version) {
244
+ throw new Error(`Manifest mismatch: expected ${name}@${version}, got ${manifest.name}@${manifest.version}.`);
245
+ }
246
+ // Fetch the signed entitlement envelope before the tarball download so we
247
+ // fail fast if the server would not recognize the grant anyway. The
248
+ // metadata call above already gates on entitlement, but this also catches
249
+ // races (e.g. revocation between the two calls) and gives us the signed
250
+ // payload gated plugins require at load time.
251
+ Logger_1.appLogger.info('Fetching signed entitlement envelope...');
252
+ const { raw: envelopeJson, envelope } = await fetchEntitlementsEnvelope(baseUrl, apiKey);
253
+ if (!envelope.payload.entitlements[name]) {
254
+ throw new Error(`Entitlement envelope does not contain a grant for ${name}. ` +
255
+ `The grant may have been revoked during install; try again.`);
256
+ }
257
+ const workDir = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'donobu-plugin-'));
258
+ try {
259
+ const tarPath = (0, path_1.join)(workDir, manifest.tarball);
260
+ Logger_1.appLogger.info(`Downloading ${manifest.tarball}...`);
261
+ await downloadTarball(baseUrl, apiKey, name, version, tarPath);
262
+ const actualSha = await sha256OfFile(tarPath);
263
+ if (actualSha.toLowerCase() !== manifest.sha256.toLowerCase()) {
264
+ throw new Error(`Checksum mismatch for ${manifest.tarball} — refusing to install. ` +
265
+ `Expected ${manifest.sha256}, got ${actualSha}.`);
266
+ }
267
+ // Extract into a staging dir inside workDir first, then atomically swap
268
+ // into the final plugins location to avoid half-written plugin state.
269
+ // License file goes inside the staging dir before the swap so the
270
+ // post-rename plugin dir always has both bundle + license present at
271
+ // the same instant — no half-installed window.
272
+ const stageDir = (0, path_1.join)(workDir, 'stage');
273
+ await extractTarball(tarPath, stageDir);
274
+ await (0, promises_1.writeFile)((0, path_1.join)(stageDir, LICENSE_FILENAME), envelopeJson + '\n', 'utf8');
275
+ const parentDir = resolveInstallParentDir(into);
276
+ const destDir = (0, path_1.join)(parentDir, name);
277
+ await (0, promises_1.mkdir)(parentDir, { recursive: true });
278
+ // For scoped names like @donobu/donobu-mobile, ensure the @scope parent exists.
279
+ await (0, promises_1.mkdir)((0, path_1.join)(destDir, '..'), { recursive: true });
280
+ await (0, promises_1.rm)(destDir, { recursive: true, force: true });
281
+ await (0, promises_1.rename)(stageDir, destDir);
282
+ const grantExpiresAt = envelope.payload.entitlements[name].expiresAt;
283
+ const expiryDescription = grantExpiresAt === null ? 'perpetual (never expires)' : grantExpiresAt;
284
+ Logger_1.appLogger.info(`Installed ${name}@${version} to ${destDir} ` +
285
+ `(license colocated, entitlement ${expiryDescription}). ` +
286
+ `Restart Donobu to load the plugin.`);
287
+ // Soft-warn when the customer is installing very close to the grant's
288
+ // expiry — catches the "I installed today but it already stopped working"
289
+ // footgun.
290
+ if (grantExpiresAt !== null) {
291
+ const daysUntilExpiry = Math.floor((new Date(grantExpiresAt).getTime() - Date.now()) / 86_400_000);
292
+ if (daysUntilExpiry >= 0 && daysUntilExpiry < 7) {
293
+ Logger_1.appLogger.warn(`Your entitlement expires in ${daysUntilExpiry} day(s) ` +
294
+ `(${grantExpiresAt}). Contact support@donobu.com to renew before relying on this install.`);
295
+ }
296
+ }
297
+ }
298
+ finally {
299
+ await (0, promises_1.rm)(workDir, { recursive: true, force: true });
300
+ }
301
+ }
302
+ function parseArgs(argv) {
303
+ const args = argv.slice(2);
304
+ let fromRelease;
305
+ let apiKey;
306
+ let into;
307
+ for (let i = 0; i < args.length; i++) {
308
+ const a = args[i];
309
+ if (a === '--from-release' && i + 1 < args.length) {
310
+ fromRelease = args[++i];
311
+ }
312
+ else if (a.startsWith('--from-release=')) {
313
+ fromRelease = a.slice('--from-release='.length);
314
+ }
315
+ else if (a === '--api-key' && i + 1 < args.length) {
316
+ apiKey = args[++i];
317
+ }
318
+ else if (a.startsWith('--api-key=')) {
319
+ apiKey = a.slice('--api-key='.length);
320
+ }
321
+ else if (a === '--into' && i + 1 < args.length) {
322
+ into = args[++i];
323
+ }
324
+ else if (a.startsWith('--into=')) {
325
+ into = a.slice('--into='.length);
326
+ }
327
+ else if (a === '--help' || a === '-h') {
328
+ printUsage();
329
+ process.exit(0);
330
+ }
331
+ }
332
+ return { fromRelease, apiKey, into };
333
+ }
334
+ function printUsage() {
335
+ console.log([
336
+ 'Usage:',
337
+ ' install-donobu-plugin # build & install from current dir',
338
+ ' install-donobu-plugin --from-release <name>[@<version>] [--api-key <key>] [--into <dir>]',
339
+ '',
340
+ 'Release-mode options:',
341
+ ' --from-release <name>[@<version>] Install a pre-built Donobu plugin.',
342
+ ' Omit @version to install the latest.',
343
+ ' --api-key <key> Donobu API key (or set DONOBU_API_KEY).',
344
+ ' Your account entitlement gates access.',
345
+ ' --into <dir> Install the plugin under <dir>/<name>/',
346
+ ' instead of the Donobu Studio plugins',
347
+ ' directory. Useful for code-based test',
348
+ ' setups — pass `./node_modules` to make',
349
+ ' the plugin importable from your project.',
350
+ '',
351
+ 'Examples:',
352
+ ' install-donobu-plugin --from-release @donobu/donobu-mobile',
353
+ ' install-donobu-plugin --from-release @donobu/donobu-mobile@1.0.0',
354
+ ' install-donobu-plugin --from-release @donobu/donobu-mobile --into ./node_modules',
355
+ ].join('\n'));
356
+ }
98
357
  async function main() {
358
+ const { fromRelease, apiKey, into } = parseArgs(process.argv);
99
359
  try {
100
- // Check if we're in a plugin directory
360
+ if (fromRelease) {
361
+ await installFromRelease(parseReleaseSpec(fromRelease), getDonobuApiKey(apiKey), into);
362
+ return;
363
+ }
364
+ // Local build-and-copy mode — must be run from a plugin source directory.
101
365
  try {
102
366
  await (0, promises_1.access)('package.json', fs_1.constants.F_OK);
103
367
  await (0, promises_1.access)('src', fs_1.constants.F_OK);
@@ -0,0 +1,36 @@
1
+ import type { Request, Response } from 'express';
2
+ import type { SuitesManager } from '../managers/SuitesManager';
3
+ import type { TestsManager } from '../managers/TestsManager';
4
+ /**
5
+ * API controller for managing suites.
6
+ */
7
+ export declare class SuitesApi {
8
+ private readonly suitesManager;
9
+ private readonly testsManager;
10
+ constructor(suitesManager: SuitesManager, testsManager: TestsManager);
11
+ /**
12
+ * GET /api/suites — list suites with optional filtering and pagination.
13
+ */
14
+ getSuites(req: Request, res: Response): Promise<void>;
15
+ /**
16
+ * POST /api/suites — create a new suite.
17
+ */
18
+ createSuite(req: Request, res: Response): Promise<void>;
19
+ /**
20
+ * GET /api/suites/:suiteId — get a suite by ID.
21
+ */
22
+ getSuite(req: Request, res: Response): Promise<void>;
23
+ /**
24
+ * PUT /api/suites/:suiteId — update an existing suite.
25
+ */
26
+ updateSuite(req: Request, res: Response): Promise<void>;
27
+ /**
28
+ * DELETE /api/suites/:suiteId — delete a suite (tests are orphaned).
29
+ */
30
+ deleteSuite(req: Request, res: Response): Promise<void>;
31
+ /**
32
+ * GET /api/suites/:suiteId/tests — list tests in a suite.
33
+ */
34
+ getSuiteTests(req: Request, res: Response): Promise<void>;
35
+ }
36
+ //# sourceMappingURL=SuitesApi.d.ts.map