berget 2.2.4 → 2.2.6
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/.github/workflows/publish.yml +2 -2
- package/.github/workflows/test.yml +1 -1
- package/dist/package.json +3 -1
- package/dist/src/commands/code/__tests__/fake-command-runner.js +52 -0
- package/dist/src/commands/code/__tests__/fake-file-store.js +46 -0
- package/dist/src/commands/code/__tests__/fake-prompter.js +91 -0
- package/dist/src/commands/code/__tests__/setup-flow.test.js +238 -0
- package/dist/src/commands/code/adapters/clack-prompter.js +71 -0
- package/dist/src/commands/code/adapters/fs-file-store.js +75 -0
- package/dist/src/commands/code/adapters/spawn-command-runner.js +49 -0
- package/dist/src/commands/code/errors.js +27 -0
- package/dist/src/commands/code/ports/command-runner.js +2 -0
- package/dist/src/commands/code/ports/file-store.js +2 -0
- package/dist/src/commands/code/ports/prompter.js +2 -0
- package/dist/src/commands/code/setup.js +392 -0
- package/dist/src/commands/code.js +189 -633
- package/dist/src/constants/command-structure.js +2 -0
- package/dist/tests/commands/code.test.js +31 -0
- package/dist/tests/utils/opencode-validator.test.js +15 -14
- package/package.json +3 -1
- package/src/commands/code/__tests__/fake-command-runner.ts +47 -0
- package/src/commands/code/__tests__/fake-file-store.ts +35 -0
- package/src/commands/code/__tests__/fake-prompter.ts +83 -0
- package/src/commands/code/__tests__/setup-flow.test.ts +274 -0
- package/src/commands/code/adapters/clack-prompter.ts +43 -0
- package/src/commands/code/adapters/fs-file-store.ts +33 -0
- package/src/commands/code/adapters/spawn-command-runner.ts +36 -0
- package/src/commands/code/errors.ts +23 -0
- package/src/commands/code/ports/command-runner.ts +6 -0
- package/src/commands/code/ports/file-store.ts +6 -0
- package/src/commands/code/ports/prompter.ts +23 -0
- package/src/commands/code/setup.ts +402 -0
- package/src/commands/code.ts +211 -748
- package/src/constants/command-structure.ts +3 -0
- package/templates/agents/app.md +22 -0
- package/templates/agents/backend.md +22 -0
- package/templates/agents/devops.md +28 -0
- package/templates/agents/frontend.md +24 -0
- package/templates/agents/fullstack.md +22 -0
- package/templates/agents/quality.md +64 -0
- package/templates/agents/security.md +20 -0
- package/tests/commands/code.test.ts +47 -0
- package/tests/utils/opencode-validator.test.ts +16 -15
- package/opencode.json +0 -146
|
@@ -14,7 +14,7 @@ on:
|
|
|
14
14
|
|
|
15
15
|
jobs:
|
|
16
16
|
test:
|
|
17
|
-
runs-on:
|
|
17
|
+
runs-on: [infra-runner-set]
|
|
18
18
|
steps:
|
|
19
19
|
- name: Checkout code
|
|
20
20
|
uses: actions/checkout@v4
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
|
|
37
37
|
publish:
|
|
38
38
|
needs: test
|
|
39
|
-
runs-on:
|
|
39
|
+
runs-on: [infra-runner-set]
|
|
40
40
|
steps:
|
|
41
41
|
- name: Checkout code
|
|
42
42
|
uses: actions/checkout@v4
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "berget",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"berget": "dist/index.js"
|
|
@@ -33,12 +33,14 @@
|
|
|
33
33
|
"vitest": "^1.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@clack/prompts": "^0.10.0",
|
|
36
37
|
"ajv": "^8.17.1",
|
|
37
38
|
"ajv-formats": "^3.0.1",
|
|
38
39
|
"chalk": "^4.1.2",
|
|
39
40
|
"commander": "^12.0.0",
|
|
40
41
|
"dotenv": "^17.2.3",
|
|
41
42
|
"fs-extra": "^11.3.0",
|
|
43
|
+
"jsonc-parser": "^3.3.1",
|
|
42
44
|
"marked": "^9.1.6",
|
|
43
45
|
"marked-terminal": "^6.2.0",
|
|
44
46
|
"open": "^9.1.0",
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.FakeCommandRunner = void 0;
|
|
13
|
+
class FakeCommandRunner {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.handlers = [];
|
|
16
|
+
this._calls = [];
|
|
17
|
+
}
|
|
18
|
+
handle(match, response) {
|
|
19
|
+
this.handlers.push({
|
|
20
|
+
match: (cmd, args) => {
|
|
21
|
+
const full = `${cmd} ${args.join(' ')}`;
|
|
22
|
+
if (typeof match === 'string')
|
|
23
|
+
return full.startsWith(match);
|
|
24
|
+
return match.test(full);
|
|
25
|
+
},
|
|
26
|
+
response,
|
|
27
|
+
});
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
checkInstalled(binary) {
|
|
31
|
+
this._calls.push({ command: `check:${binary}`, args: [] });
|
|
32
|
+
return Promise.resolve(this.handlers.some(h => h.match(binary, ['--version'])) || false);
|
|
33
|
+
}
|
|
34
|
+
run(command, args, options) {
|
|
35
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
this._calls.push({ command, args: [...args], options });
|
|
37
|
+
const handler = this.handlers.find(h => h.match(command, args));
|
|
38
|
+
if (!handler)
|
|
39
|
+
throw new Error(`Unexpected command: ${command} ${args.join(' ')}`);
|
|
40
|
+
const result = typeof handler.response === 'function'
|
|
41
|
+
? handler.response(command, args)
|
|
42
|
+
: handler.response;
|
|
43
|
+
if (result instanceof Error)
|
|
44
|
+
throw result;
|
|
45
|
+
return result;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
get calls() {
|
|
49
|
+
return this._calls;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.FakeCommandRunner = FakeCommandRunner;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.FakeFileStore = void 0;
|
|
13
|
+
class FakeFileStore {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.files = new Map();
|
|
16
|
+
this.dirs = new Set();
|
|
17
|
+
}
|
|
18
|
+
seed(path, content) {
|
|
19
|
+
this.files.set(path, content);
|
|
20
|
+
}
|
|
21
|
+
exists(path) {
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
return this.files.has(path) || this.dirs.has(path);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
readFile(path) {
|
|
27
|
+
var _a;
|
|
28
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29
|
+
return (_a = this.files.get(path)) !== null && _a !== void 0 ? _a : null;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
writeFile(path, content) {
|
|
33
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
this.files.set(path, content);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
mkdir(path) {
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
this.dirs.add(path);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
getWrittenFiles() {
|
|
43
|
+
return new Map(this.files);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.FakeFileStore = FakeFileStore;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.FakePrompter = exports.confirm = exports.select = exports.CANCEL = void 0;
|
|
13
|
+
const errors_1 = require("../errors");
|
|
14
|
+
exports.CANCEL = Symbol('cancel');
|
|
15
|
+
const select = (value, match) => ({
|
|
16
|
+
kind: 'select',
|
|
17
|
+
match: typeof match === 'string' ? new RegExp(match) : match,
|
|
18
|
+
response: typeof value === 'symbol' ? value : String(value),
|
|
19
|
+
});
|
|
20
|
+
exports.select = select;
|
|
21
|
+
const confirm = (value, match) => ({
|
|
22
|
+
kind: 'confirm',
|
|
23
|
+
match: typeof match === 'string' ? new RegExp(match) : match,
|
|
24
|
+
response: value,
|
|
25
|
+
});
|
|
26
|
+
exports.confirm = confirm;
|
|
27
|
+
class FakePrompter {
|
|
28
|
+
constructor(_script) {
|
|
29
|
+
this._script = _script;
|
|
30
|
+
this._calls = [];
|
|
31
|
+
this._cursor = 0;
|
|
32
|
+
}
|
|
33
|
+
intro(message) {
|
|
34
|
+
this._calls.push({ method: 'intro', args: { message } });
|
|
35
|
+
}
|
|
36
|
+
outro(message) {
|
|
37
|
+
this._calls.push({ method: 'outro', args: { message } });
|
|
38
|
+
}
|
|
39
|
+
note(message, title) {
|
|
40
|
+
this._calls.push({ method: 'note', args: { message, title } });
|
|
41
|
+
}
|
|
42
|
+
spinner() {
|
|
43
|
+
return {
|
|
44
|
+
start: (msg) => {
|
|
45
|
+
this._calls.push({ method: 'spinner.start', args: { message: msg } });
|
|
46
|
+
},
|
|
47
|
+
stop: (msg) => {
|
|
48
|
+
this._calls.push({ method: 'spinner.stop', args: { message: msg } });
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
select(opts) {
|
|
53
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
54
|
+
this._calls.push({ method: 'select', args: opts });
|
|
55
|
+
const entry = this._script[this._cursor++];
|
|
56
|
+
if (!entry)
|
|
57
|
+
throw new Error(`No script entry for select #${this._cursor} (${opts.message})`);
|
|
58
|
+
if (entry.kind !== 'select')
|
|
59
|
+
throw new Error(`Expected confirm, got select for ${opts.message}`);
|
|
60
|
+
if (entry.match && !entry.match.test(opts.message))
|
|
61
|
+
throw new Error(`Message mismatch: got "${opts.message}"`);
|
|
62
|
+
if (entry.response === exports.CANCEL)
|
|
63
|
+
throw new errors_1.CancelledError();
|
|
64
|
+
return entry.response;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
confirm(opts) {
|
|
68
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
this._calls.push({ method: 'confirm', args: opts });
|
|
70
|
+
const entry = this._script[this._cursor++];
|
|
71
|
+
if (!entry)
|
|
72
|
+
throw new Error(`No script entry for confirm #${this._cursor} (${opts.message})`);
|
|
73
|
+
if (entry.kind !== 'confirm')
|
|
74
|
+
throw new Error(`Expected select, got confirm for ${opts.message}`);
|
|
75
|
+
if (entry.match && !entry.match.test(opts.message))
|
|
76
|
+
throw new Error(`Message mismatch: got "${opts.message}"`);
|
|
77
|
+
if (entry.response === exports.CANCEL)
|
|
78
|
+
throw new errors_1.CancelledError();
|
|
79
|
+
return entry.response;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
get calls() {
|
|
83
|
+
return this._calls;
|
|
84
|
+
}
|
|
85
|
+
assertExhausted() {
|
|
86
|
+
if (this._cursor !== this._script.length) {
|
|
87
|
+
throw new Error(`Script not exhausted: ${this._script.length - this._cursor} entries left`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.FakePrompter = FakePrompter;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const vitest_1 = require("vitest");
|
|
13
|
+
const setup_1 = require("../setup");
|
|
14
|
+
const errors_1 = require("../errors");
|
|
15
|
+
const fake_prompter_1 = require("./fake-prompter");
|
|
16
|
+
const fake_file_store_1 = require("./fake-file-store");
|
|
17
|
+
const fake_command_runner_1 = require("./fake-command-runner");
|
|
18
|
+
const makeDeps = (overrides = {}) => (Object.assign({ prompter: new fake_prompter_1.FakePrompter([]), files: new fake_file_store_1.FakeFileStore(), commands: new fake_command_runner_1.FakeCommandRunner()
|
|
19
|
+
.handle('opencode --version', 'mocked')
|
|
20
|
+
.handle('pi --version', 'mocked'), homeDir: '/home/user', cwd: '/home/user/project' }, overrides));
|
|
21
|
+
(0, vitest_1.describe)('runSetup', () => {
|
|
22
|
+
(0, vitest_1.describe)('happy path', () => {
|
|
23
|
+
(0, vitest_1.it)('sets up opencode project without existing config', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
|
+
const deps = makeDeps({
|
|
25
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
26
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
27
|
+
(0, fake_prompter_1.select)('project'),
|
|
28
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
29
|
+
]),
|
|
30
|
+
});
|
|
31
|
+
yield (0, setup_1.runSetup)(deps);
|
|
32
|
+
const files = deps.files;
|
|
33
|
+
const written = files.getWrittenFiles();
|
|
34
|
+
(0, vitest_1.expect)(written.has('/home/user/project/opencode.json')).toBe(true);
|
|
35
|
+
const config = JSON.parse(written.get('/home/user/project/opencode.json'));
|
|
36
|
+
(0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth@1.0.16');
|
|
37
|
+
}));
|
|
38
|
+
(0, vitest_1.it)('sets up opencode globally without existing config', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
const deps = makeDeps({
|
|
40
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
41
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
42
|
+
(0, fake_prompter_1.select)('global'),
|
|
43
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
44
|
+
]),
|
|
45
|
+
});
|
|
46
|
+
yield (0, setup_1.runSetup)(deps);
|
|
47
|
+
const files = deps.files;
|
|
48
|
+
const written = files.getWrittenFiles();
|
|
49
|
+
(0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
|
|
50
|
+
}));
|
|
51
|
+
(0, vitest_1.it)('sets up pi project with fresh install', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
52
|
+
const deps = makeDeps({
|
|
53
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
54
|
+
(0, fake_prompter_1.select)('pi'),
|
|
55
|
+
(0, fake_prompter_1.select)('project'),
|
|
56
|
+
(0, fake_prompter_1.confirm)(true, 'Proceed'),
|
|
57
|
+
]),
|
|
58
|
+
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
59
|
+
.handle('pi --version', 'mocked') // For checkInstalled
|
|
60
|
+
.handle('pi install', ''), // For actual install
|
|
61
|
+
});
|
|
62
|
+
yield (0, setup_1.runSetup)(deps);
|
|
63
|
+
const commands = deps.commands;
|
|
64
|
+
(0, vitest_1.expect)(commands.calls.length).toBeGreaterThan(0);
|
|
65
|
+
const installCall = commands.calls.find(c => c.command === 'pi');
|
|
66
|
+
(0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain('npm:@bergetai/pi-provider');
|
|
67
|
+
}));
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.describe)('prerequisites', () => {
|
|
70
|
+
(0, vitest_1.it)('throws PrerequisiteError when opencode is not installed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
71
|
+
const deps = makeDeps({
|
|
72
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
73
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
74
|
+
(0, fake_prompter_1.select)('project'),
|
|
75
|
+
]),
|
|
76
|
+
commands: new fake_command_runner_1.FakeCommandRunner(),
|
|
77
|
+
});
|
|
78
|
+
// Simulate opencode not being installed
|
|
79
|
+
yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.PrerequisiteError);
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
(0, vitest_1.describe)('cancellation', () => {
|
|
83
|
+
(0, vitest_1.it)('throws CancelledError when user cancels at tool selection', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
84
|
+
const deps = makeDeps({
|
|
85
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
86
|
+
(0, fake_prompter_1.select)(fake_prompter_1.CANCEL),
|
|
87
|
+
]),
|
|
88
|
+
});
|
|
89
|
+
yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
|
|
90
|
+
}));
|
|
91
|
+
(0, vitest_1.it)('throws CancelledError when user cancels at write confirmation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
const deps = makeDeps({
|
|
93
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
94
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
95
|
+
(0, fake_prompter_1.select)('project'),
|
|
96
|
+
(0, fake_prompter_1.confirm)(false, 'Create'),
|
|
97
|
+
]),
|
|
98
|
+
});
|
|
99
|
+
yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CancelledError);
|
|
100
|
+
}));
|
|
101
|
+
});
|
|
102
|
+
(0, vitest_1.describe)('file operations', () => {
|
|
103
|
+
(0, vitest_1.it)('preserves existing configuration keys when updating', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
const deps = makeDeps({
|
|
105
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
106
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
107
|
+
(0, fake_prompter_1.select)('project'),
|
|
108
|
+
(0, fake_prompter_1.confirm)(true, 'Write'),
|
|
109
|
+
]),
|
|
110
|
+
});
|
|
111
|
+
const files = deps.files;
|
|
112
|
+
files.seed('/home/user/project/opencode.json', JSON.stringify({
|
|
113
|
+
customField: 'should-preserve',
|
|
114
|
+
plugin: ['other-plugin'],
|
|
115
|
+
}));
|
|
116
|
+
yield (0, setup_1.runSetup)(deps);
|
|
117
|
+
const written = files.getWrittenFiles();
|
|
118
|
+
const config = JSON.parse(written.get('/home/user/project/opencode.json'));
|
|
119
|
+
(0, vitest_1.expect)(config.customField).toBe('should-preserve');
|
|
120
|
+
(0, vitest_1.expect)(config.plugin).toContain('other-plugin');
|
|
121
|
+
(0, vitest_1.expect)(config.plugin).toContain('@bergetai/opencode-auth@1.0.16');
|
|
122
|
+
}));
|
|
123
|
+
(0, vitest_1.it)('preserves jsonc comments when updating', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
const deps = makeDeps({
|
|
125
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
126
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
127
|
+
(0, fake_prompter_1.select)('project'),
|
|
128
|
+
(0, fake_prompter_1.confirm)(true, 'Write'),
|
|
129
|
+
]),
|
|
130
|
+
});
|
|
131
|
+
const files = deps.files;
|
|
132
|
+
files.seed('/home/user/project/opencode.jsonc', `{
|
|
133
|
+
// This is my custom config
|
|
134
|
+
"customField": "should-preserve",
|
|
135
|
+
/* block comment explaining plugin */
|
|
136
|
+
"plugin": ["other-plugin"]
|
|
137
|
+
}`);
|
|
138
|
+
yield (0, setup_1.runSetup)(deps);
|
|
139
|
+
const written = files.getWrittenFiles();
|
|
140
|
+
const content = written.get('/home/user/project/opencode.jsonc');
|
|
141
|
+
(0, vitest_1.expect)(content).toContain('// This is my custom config');
|
|
142
|
+
(0, vitest_1.expect)(content).toContain('/* block comment explaining plugin */');
|
|
143
|
+
(0, vitest_1.expect)(content).toContain('"customField": "should-preserve"');
|
|
144
|
+
(0, vitest_1.expect)(content).toContain('@bergetai/opencode-auth@1.0.16');
|
|
145
|
+
}));
|
|
146
|
+
(0, vitest_1.it)('shows no changes needed when config is already up to date', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
147
|
+
const deps = makeDeps({
|
|
148
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
149
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
150
|
+
(0, fake_prompter_1.select)('project'),
|
|
151
|
+
]),
|
|
152
|
+
});
|
|
153
|
+
const files = deps.files;
|
|
154
|
+
// Already has the exact same plugin version
|
|
155
|
+
files.seed('/home/user/project/opencode.json', JSON.stringify({
|
|
156
|
+
$schema: 'https://opencode.ai/config.json',
|
|
157
|
+
plugin: ['@bergetai/opencode-auth@1.0.16'],
|
|
158
|
+
}, null, 2) + '\n');
|
|
159
|
+
yield (0, setup_1.runSetup)(deps);
|
|
160
|
+
// Check that no write happened — content should be unchanged
|
|
161
|
+
const written = files.getWrittenFiles();
|
|
162
|
+
const content = written.get('/home/user/project/opencode.json');
|
|
163
|
+
const config = JSON.parse(content);
|
|
164
|
+
(0, vitest_1.expect)(config.plugin).toEqual(['@bergetai/opencode-auth@1.0.16']);
|
|
165
|
+
(0, vitest_1.expect)(content).toContain('$schema');
|
|
166
|
+
}));
|
|
167
|
+
(0, vitest_1.it)('preserves existing Pi settings when setting defaultProvider', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
168
|
+
const deps = makeDeps({
|
|
169
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
170
|
+
(0, fake_prompter_1.select)('pi'),
|
|
171
|
+
(0, fake_prompter_1.select)('project'),
|
|
172
|
+
(0, fake_prompter_1.confirm)(true, 'Proceed'),
|
|
173
|
+
]),
|
|
174
|
+
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
175
|
+
.handle('pi --version', 'mocked')
|
|
176
|
+
.handle('pi install', ''),
|
|
177
|
+
});
|
|
178
|
+
const files = deps.files;
|
|
179
|
+
files.seed('/home/user/project/.pi/settings.json', JSON.stringify({
|
|
180
|
+
existingKey: 'should-preserve',
|
|
181
|
+
anotherSetting: true,
|
|
182
|
+
}));
|
|
183
|
+
yield (0, setup_1.runSetup)(deps);
|
|
184
|
+
const written = files.getWrittenFiles();
|
|
185
|
+
const settings = JSON.parse(written.get('/home/user/project/.pi/settings.json'));
|
|
186
|
+
(0, vitest_1.expect)(settings.existingKey).toBe('should-preserve');
|
|
187
|
+
(0, vitest_1.expect)(settings.anotherSetting).toBe(true);
|
|
188
|
+
(0, vitest_1.expect)(settings.defaultProvider).toBe('berget');
|
|
189
|
+
}));
|
|
190
|
+
(0, vitest_1.it)('creates parent directories when writing files', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
191
|
+
const deps = makeDeps({
|
|
192
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
193
|
+
(0, fake_prompter_1.select)('opencode'),
|
|
194
|
+
(0, fake_prompter_1.select)('global'),
|
|
195
|
+
(0, fake_prompter_1.confirm)(true, 'Create'),
|
|
196
|
+
]),
|
|
197
|
+
});
|
|
198
|
+
yield (0, setup_1.runSetup)(deps);
|
|
199
|
+
const files = deps.files;
|
|
200
|
+
const written = files.getWrittenFiles();
|
|
201
|
+
(0, vitest_1.expect)(written.has('/home/user/.config/opencode/opencode.json')).toBe(true);
|
|
202
|
+
}));
|
|
203
|
+
});
|
|
204
|
+
(0, vitest_1.describe)('command execution', () => {
|
|
205
|
+
(0, vitest_1.it)('passes arguments as array (no shell injection)', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
206
|
+
const deps = makeDeps({
|
|
207
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
208
|
+
(0, fake_prompter_1.select)('pi'),
|
|
209
|
+
(0, fake_prompter_1.select)('project'),
|
|
210
|
+
(0, fake_prompter_1.confirm)(true, 'Proceed'),
|
|
211
|
+
]),
|
|
212
|
+
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
213
|
+
.handle('pi --version', 'mocked')
|
|
214
|
+
.handle('pi install', ''),
|
|
215
|
+
});
|
|
216
|
+
yield (0, setup_1.runSetup)(deps);
|
|
217
|
+
const commands = deps.commands;
|
|
218
|
+
const installCall = commands.calls.find(c => c.command === 'pi');
|
|
219
|
+
(0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain('npm:@bergetai/pi-provider');
|
|
220
|
+
(0, vitest_1.expect)(installCall === null || installCall === void 0 ? void 0 : installCall.args).toContain('-l');
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
(0, vitest_1.describe)('error handling', () => {
|
|
224
|
+
(0, vitest_1.it)('throws CommandFailedError when pi install fails', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
225
|
+
const deps = makeDeps({
|
|
226
|
+
prompter: new fake_prompter_1.FakePrompter([
|
|
227
|
+
(0, fake_prompter_1.select)('pi'),
|
|
228
|
+
(0, fake_prompter_1.select)('project'),
|
|
229
|
+
(0, fake_prompter_1.confirm)(true, 'Proceed'),
|
|
230
|
+
]),
|
|
231
|
+
commands: new fake_command_runner_1.FakeCommandRunner()
|
|
232
|
+
.handle('pi --version', 'mocked')
|
|
233
|
+
.handle('pi install', new Error('npm error')),
|
|
234
|
+
});
|
|
235
|
+
yield (0, vitest_1.expect)((0, setup_1.runSetup)(deps)).rejects.toBeInstanceOf(errors_1.CommandFailedError);
|
|
236
|
+
}));
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.ClackPrompter = void 0;
|
|
36
|
+
const p = __importStar(require("@clack/prompts"));
|
|
37
|
+
const errors_1 = require("../errors");
|
|
38
|
+
const unwrap = (v) => {
|
|
39
|
+
if (p.isCancel(v))
|
|
40
|
+
throw new errors_1.CancelledError();
|
|
41
|
+
return v;
|
|
42
|
+
};
|
|
43
|
+
class ClackPrompter {
|
|
44
|
+
intro(message) {
|
|
45
|
+
p.intro(message);
|
|
46
|
+
}
|
|
47
|
+
outro(message) {
|
|
48
|
+
p.outro(message);
|
|
49
|
+
}
|
|
50
|
+
note(message, title) {
|
|
51
|
+
p.note(message, title);
|
|
52
|
+
}
|
|
53
|
+
spinner() {
|
|
54
|
+
const s = p.spinner();
|
|
55
|
+
return {
|
|
56
|
+
start: (msg) => s.start(msg),
|
|
57
|
+
stop: (msg) => s.stop(msg),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
select(opts) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
return unwrap(yield p.select(opts));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
confirm(opts) {
|
|
66
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
return unwrap(yield p.confirm(opts));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.ClackPrompter = ClackPrompter;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.FsFileStore = void 0;
|
|
36
|
+
const node_fs_1 = require("node:fs");
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
class FsFileStore {
|
|
39
|
+
exists(filePath) {
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
try {
|
|
42
|
+
yield node_fs_1.promises.access(filePath);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
catch (_a) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
readFile(filePath) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
try {
|
|
53
|
+
return yield node_fs_1.promises.readFile(filePath, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (err.code === 'ENOENT')
|
|
57
|
+
return null;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
writeFile(filePath, content) {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
const dir = path.dirname(filePath);
|
|
65
|
+
yield node_fs_1.promises.mkdir(dir, { recursive: true });
|
|
66
|
+
yield node_fs_1.promises.writeFile(filePath, content, 'utf8');
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
mkdir(dir) {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
yield node_fs_1.promises.mkdir(dir, { recursive: true });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.FsFileStore = FsFileStore;
|