@decohere/scaffold 0.0.1-dev.100
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/commonjs/src/add-api.action.js +37 -0
- package/commonjs/src/add-api.command.js +39 -0
- package/commonjs/src/add.command.js +22 -0
- package/commonjs/src/add.module.js +24 -0
- package/commonjs/src/auth0-configure.action.js +28 -0
- package/commonjs/src/auth0-configure.command.js +24 -0
- package/commonjs/src/auth0-login.action.js +28 -0
- package/commonjs/src/auth0-login.command.js +23 -0
- package/commonjs/src/auth0.command.js +22 -0
- package/commonjs/src/auth0.module.js +28 -0
- package/commonjs/src/cli.js +7 -0
- package/commonjs/src/create.command.js +22 -0
- package/commonjs/src/create.module.js +24 -0
- package/commonjs/src/domain/prompt.js +62 -0
- package/commonjs/src/index.js +48 -0
- package/commonjs/src/models/compose.model.js +258 -0
- package/commonjs/src/models/config-yaml.model.js +86 -0
- package/commonjs/src/models/const.js +30 -0
- package/commonjs/src/models/dev-container.model.js +172 -0
- package/commonjs/src/models/docker-image.model.js +277 -0
- package/commonjs/src/models/env-file.model.js +48 -0
- package/commonjs/src/models/gitignore.model.js +48 -0
- package/commonjs/src/models/launch-json.model.js +70 -0
- package/commonjs/src/models/package-json.model.js +156 -0
- package/commonjs/src/models/project.model.js +132 -0
- package/commonjs/src/models/source-file.js +4 -0
- package/commonjs/src/models/tsconfig.model.js +32 -0
- package/commonjs/src/models/vitest-config.model.js +33 -0
- package/commonjs/src/models/vscode-settings.model.js +29 -0
- package/commonjs/src/program.js +64 -0
- package/commonjs/src/resolve-target-dir.js +17 -0
- package/commonjs/src/services/scaffold.service.js +60 -0
- package/commonjs/src/version.js +55 -0
- package/commonjs/src/webapi/api-name.js +93 -0
- package/commonjs/src/webapi/auth0-commands.js +250 -0
- package/commonjs/src/webapi/authorize-scopes.js +79 -0
- package/commonjs/src/webapi/cli-options.js +99 -0
- package/commonjs/src/webapi/const.js +54 -0
- package/commonjs/src/webapi/create-api.js +265 -0
- package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.js +164 -0
- package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
- package/commonjs/src/webapi/devcontainer/host-scripts.js +50 -0
- package/commonjs/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
- package/commonjs/src/webapi/devcontainer/templates/initialize.sh +169 -0
- package/commonjs/src/webapi/devcontainer/templates/post-create.sh +35 -0
- package/commonjs/src/webapi/devcontainer/templates/post-start.sh +65 -0
- package/commonjs/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
- package/commonjs/src/webapi/features/auth.feature.js +408 -0
- package/commonjs/src/webapi/features/auth0-cli.js +472 -0
- package/commonjs/src/webapi/features/biome-format.js +47 -0
- package/commonjs/src/webapi/features/database.feature.js +432 -0
- package/commonjs/src/webapi/features/finalize.feature.js +93 -0
- package/commonjs/src/webapi/features/postman.feature.js +322 -0
- package/commonjs/src/webapi/models/api-package-json.model.js +58 -0
- package/commonjs/src/webapi/models/auth-config.model.js +24 -0
- package/commonjs/src/webapi/models/controller-source-file.model.js +190 -0
- package/commonjs/src/webapi/models/github-ci.model.js +193 -0
- package/commonjs/src/webapi/models/main-source-file.model.js +165 -0
- package/commonjs/src/webapi/models/production-dockerfile.model.js +75 -0
- package/commonjs/src/webapi/models/sample-files.model.js +58 -0
- package/commonjs/src/webapi/models/webapi-image.model.js +25 -0
- package/commonjs/src/webapi/postman/hello-openapi.js +173 -0
- package/commonjs/src/webapi/postman/local-view.js +182 -0
- package/commonjs/src/webapi/postman/openapi-to-postman.js +200 -0
- package/commonjs/src/webapi/workspace-readme.js +109 -0
- package/commonjs/src/webapi.action.js +28 -0
- package/commonjs/src/webapi.command.js +43 -0
- package/es6/src/add-api.action.js +27 -0
- package/es6/src/add-api.command.js +29 -0
- package/es6/src/add.command.js +12 -0
- package/es6/src/add.module.js +14 -0
- package/es6/src/auth0-configure.action.js +18 -0
- package/es6/src/auth0-configure.command.js +14 -0
- package/es6/src/auth0-login.action.js +18 -0
- package/es6/src/auth0-login.command.js +13 -0
- package/es6/src/auth0.command.js +12 -0
- package/es6/src/auth0.module.js +18 -0
- package/es6/src/cli.js +3 -0
- package/es6/src/create.command.js +12 -0
- package/es6/src/create.module.js +14 -0
- package/es6/src/domain/prompt.js +41 -0
- package/es6/src/index.js +9 -0
- package/es6/src/models/compose.model.js +254 -0
- package/es6/src/models/config-yaml.model.js +74 -0
- package/es6/src/models/const.js +20 -0
- package/es6/src/models/dev-container.model.js +162 -0
- package/es6/src/models/docker-image.model.js +235 -0
- package/es6/src/models/env-file.model.js +44 -0
- package/es6/src/models/gitignore.model.js +38 -0
- package/es6/src/models/launch-json.model.js +60 -0
- package/es6/src/models/package-json.model.js +141 -0
- package/es6/src/models/project.model.js +117 -0
- package/es6/src/models/source-file.js +1 -0
- package/es6/src/models/tsconfig.model.js +22 -0
- package/es6/src/models/vitest-config.model.js +23 -0
- package/es6/src/models/vscode-settings.model.js +19 -0
- package/es6/src/program.js +46 -0
- package/es6/src/resolve-target-dir.js +7 -0
- package/es6/src/services/scaffold.service.js +50 -0
- package/es6/src/version.js +34 -0
- package/es6/src/webapi/api-name.js +57 -0
- package/es6/src/webapi/auth0-commands.js +185 -0
- package/es6/src/webapi/authorize-scopes.js +55 -0
- package/es6/src/webapi/cli-options.js +34 -0
- package/es6/src/webapi/const.js +21 -0
- package/es6/src/webapi/create-api.js +245 -0
- package/es6/src/webapi/devcontainer/codebase-rag-mcp.js +154 -0
- package/es6/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
- package/es6/src/webapi/devcontainer/host-scripts.js +29 -0
- package/es6/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
- package/es6/src/webapi/devcontainer/templates/initialize.sh +169 -0
- package/es6/src/webapi/devcontainer/templates/post-create.sh +35 -0
- package/es6/src/webapi/devcontainer/templates/post-start.sh +65 -0
- package/es6/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
- package/es6/src/webapi/features/auth.feature.js +357 -0
- package/es6/src/webapi/features/auth0-cli.js +406 -0
- package/es6/src/webapi/features/biome-format.js +40 -0
- package/es6/src/webapi/features/database.feature.js +422 -0
- package/es6/src/webapi/features/finalize.feature.js +83 -0
- package/es6/src/webapi/features/postman.feature.js +256 -0
- package/es6/src/webapi/models/api-package-json.model.js +53 -0
- package/es6/src/webapi/models/auth-config.model.js +20 -0
- package/es6/src/webapi/models/controller-source-file.model.js +181 -0
- package/es6/src/webapi/models/github-ci.model.js +186 -0
- package/es6/src/webapi/models/main-source-file.model.js +161 -0
- package/es6/src/webapi/models/production-dockerfile.model.js +66 -0
- package/es6/src/webapi/models/sample-files.model.js +40 -0
- package/es6/src/webapi/models/webapi-image.model.js +18 -0
- package/es6/src/webapi/postman/hello-openapi.js +166 -0
- package/es6/src/webapi/postman/local-view.js +167 -0
- package/es6/src/webapi/postman/openapi-to-postman.js +177 -0
- package/es6/src/webapi/workspace-readme.js +88 -0
- package/es6/src/webapi.action.js +18 -0
- package/es6/src/webapi.command.js +33 -0
- package/package.json +36 -0
- package/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
- package/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
- package/src/webapi/devcontainer/templates/initialize.sh +169 -0
- package/src/webapi/devcontainer/templates/post-create.sh +35 -0
- package/src/webapi/devcontainer/templates/post-start.sh +65 -0
- package/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
- package/types/src/add-api.action.d.ts +13 -0
- package/types/src/add-api.command.d.ts +7 -0
- package/types/src/add.command.d.ts +6 -0
- package/types/src/add.module.d.ts +14 -0
- package/types/src/auth0-configure.action.d.ts +7 -0
- package/types/src/auth0-configure.command.d.ts +6 -0
- package/types/src/auth0-login.action.d.ts +4 -0
- package/types/src/auth0-login.command.d.ts +6 -0
- package/types/src/auth0.command.d.ts +6 -0
- package/types/src/auth0.module.d.ts +18 -0
- package/types/src/cli.d.ts +2 -0
- package/types/src/create.command.d.ts +6 -0
- package/types/src/create.module.d.ts +14 -0
- package/types/src/domain/prompt.d.ts +4 -0
- package/types/src/index.d.ts +13 -0
- package/types/src/models/compose.model.d.ts +73 -0
- package/types/src/models/config-yaml.model.d.ts +27 -0
- package/types/src/models/const.d.ts +20 -0
- package/types/src/models/dev-container.model.d.ts +49 -0
- package/types/src/models/docker-image.model.d.ts +90 -0
- package/types/src/models/env-file.model.d.ts +16 -0
- package/types/src/models/gitignore.model.d.ts +9 -0
- package/types/src/models/launch-json.model.d.ts +7 -0
- package/types/src/models/package-json.model.d.ts +36 -0
- package/types/src/models/project.model.d.ts +60 -0
- package/types/src/models/source-file.d.ts +11 -0
- package/types/src/models/tsconfig.model.d.ts +5 -0
- package/types/src/models/vitest-config.model.d.ts +5 -0
- package/types/src/models/vscode-settings.model.d.ts +7 -0
- package/types/src/program.d.ts +3 -0
- package/types/src/resolve-target-dir.d.ts +5 -0
- package/types/src/services/scaffold.service.d.ts +22 -0
- package/types/src/version.d.ts +6 -0
- package/types/src/webapi/api-name.d.ts +14 -0
- package/types/src/webapi/auth0-commands.d.ts +11 -0
- package/types/src/webapi/authorize-scopes.d.ts +7 -0
- package/types/src/webapi/cli-options.d.ts +6 -0
- package/types/src/webapi/const.d.ts +19 -0
- package/types/src/webapi/create-api.d.ts +15 -0
- package/types/src/webapi/devcontainer/codebase-rag-mcp.d.ts +32 -0
- package/types/src/webapi/devcontainer/codebase-rag-mcp.ts +182 -0
- package/types/src/webapi/devcontainer/host-scripts.d.ts +7 -0
- package/types/src/webapi/devcontainer/templates/codebase-rag.env.example +3 -0
- package/types/src/webapi/devcontainer/templates/initialize.sh +169 -0
- package/types/src/webapi/devcontainer/templates/post-create.sh +35 -0
- package/types/src/webapi/devcontainer/templates/post-start.sh +65 -0
- package/types/src/webapi/devcontainer/templates/tty-prompt.sh +38 -0
- package/types/src/webapi/features/auth.feature.d.ts +10 -0
- package/types/src/webapi/features/auth0-cli.d.ts +84 -0
- package/types/src/webapi/features/biome-format.d.ts +5 -0
- package/types/src/webapi/features/database.feature.d.ts +7 -0
- package/types/src/webapi/features/finalize.feature.d.ts +2 -0
- package/types/src/webapi/features/postman.feature.d.ts +16 -0
- package/types/src/webapi/models/api-package-json.model.d.ts +18 -0
- package/types/src/webapi/models/auth-config.model.d.ts +26 -0
- package/types/src/webapi/models/controller-source-file.model.d.ts +46 -0
- package/types/src/webapi/models/github-ci.model.d.ts +9 -0
- package/types/src/webapi/models/main-source-file.model.d.ts +54 -0
- package/types/src/webapi/models/production-dockerfile.model.d.ts +18 -0
- package/types/src/webapi/models/sample-files.model.d.ts +7 -0
- package/types/src/webapi/models/webapi-image.model.d.ts +12 -0
- package/types/src/webapi/postman/hello-openapi.d.ts +19 -0
- package/types/src/webapi/postman/local-view.d.ts +17 -0
- package/types/src/webapi/postman/openapi-to-postman.d.ts +62 -0
- package/types/src/webapi/workspace-readme.d.ts +18 -0
- package/types/src/webapi.action.d.ts +14 -0
- package/types/src/webapi.command.d.ts +11 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DecohereCommand } from "@decohere/nexus/cli";
|
|
2
|
+
import { Auth0ConfigureAction } from "./auth0-configure.action.js";
|
|
3
|
+
export class Auth0ConfigureCommand extends DecohereCommand {
|
|
4
|
+
get actions() {
|
|
5
|
+
return [
|
|
6
|
+
Auth0ConfigureAction
|
|
7
|
+
];
|
|
8
|
+
}
|
|
9
|
+
constructor(name){
|
|
10
|
+
super(name);
|
|
11
|
+
this.description("Update Auth0 API permissions and Postman SPA from @Authorize in this workspace");
|
|
12
|
+
this.option("-d <directory>", "Workspace directory (default: current directory)", ".");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
import { injectable } from "@decohere/crucible/decorators";
|
|
8
|
+
import { ActionResponse } from "@decohere/nexus/cli";
|
|
9
|
+
import { runAuth0Login } from "./webapi/auth0-commands.js";
|
|
10
|
+
export class Auth0LoginAction {
|
|
11
|
+
async run() {
|
|
12
|
+
runAuth0Login();
|
|
13
|
+
return ActionResponse.Success("login");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
Auth0LoginAction = _ts_decorate([
|
|
17
|
+
injectable()
|
|
18
|
+
], Auth0LoginAction);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DecohereCommand } from "@decohere/nexus/cli";
|
|
2
|
+
import { Auth0LoginAction } from "./auth0-login.action.js";
|
|
3
|
+
export class Auth0LoginCommand extends DecohereCommand {
|
|
4
|
+
get actions() {
|
|
5
|
+
return [
|
|
6
|
+
Auth0LoginAction
|
|
7
|
+
];
|
|
8
|
+
}
|
|
9
|
+
constructor(name){
|
|
10
|
+
super(name);
|
|
11
|
+
this.description("Authenticate the Auth0 CLI (device URL; tokens persist on the host)");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DecohereCommand } from "@decohere/nexus/cli";
|
|
2
|
+
export class Auth0Command extends DecohereCommand {
|
|
3
|
+
constructor(commands, handler){
|
|
4
|
+
super(Auth0Command);
|
|
5
|
+
this.description("Log in to Auth0 and update a scaffolded client");
|
|
6
|
+
this.exitOverride();
|
|
7
|
+
this.registerSubcommand(commands, handler);
|
|
8
|
+
}
|
|
9
|
+
run(_actions, _options) {
|
|
10
|
+
return this.help();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ModuleManifest } from "@decohere/nexus/cli";
|
|
2
|
+
import { Auth0Command } from "./auth0.command.js";
|
|
3
|
+
import { Auth0ConfigureAction } from "./auth0-configure.action.js";
|
|
4
|
+
import { Auth0ConfigureCommand } from "./auth0-configure.command.js";
|
|
5
|
+
import { Auth0LoginAction } from "./auth0-login.action.js";
|
|
6
|
+
import { Auth0LoginCommand } from "./auth0-login.command.js";
|
|
7
|
+
export class Auth0Module extends ModuleManifest {
|
|
8
|
+
id = "auth0";
|
|
9
|
+
command = Auth0Command;
|
|
10
|
+
commands = {
|
|
11
|
+
login: Auth0LoginCommand,
|
|
12
|
+
configure: Auth0ConfigureCommand
|
|
13
|
+
};
|
|
14
|
+
actions = {
|
|
15
|
+
login: Auth0LoginAction,
|
|
16
|
+
configure: Auth0ConfigureAction
|
|
17
|
+
};
|
|
18
|
+
}
|
package/es6/src/cli.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DecohereCommand } from "@decohere/nexus/cli";
|
|
2
|
+
export class CreateCommand extends DecohereCommand {
|
|
3
|
+
constructor(commands, handler){
|
|
4
|
+
super(CreateCommand);
|
|
5
|
+
this.description("Create a new Decohere workspace");
|
|
6
|
+
this.exitOverride();
|
|
7
|
+
this.registerSubcommand(commands, handler);
|
|
8
|
+
}
|
|
9
|
+
run(_actions, _options) {
|
|
10
|
+
return this.help();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ModuleManifest } from "@decohere/nexus/cli";
|
|
2
|
+
import { CreateCommand } from "./create.command.js";
|
|
3
|
+
import { WebapiAction } from "./webapi.action.js";
|
|
4
|
+
import { WebapiCommand } from "./webapi.command.js";
|
|
5
|
+
export class CreateModule extends ModuleManifest {
|
|
6
|
+
id = "create";
|
|
7
|
+
command = CreateCommand;
|
|
8
|
+
commands = {
|
|
9
|
+
webapi: WebapiCommand
|
|
10
|
+
};
|
|
11
|
+
actions = {
|
|
12
|
+
webapi: WebapiAction
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
// readline/promises rl.question() has a race condition with piped (non-TTY) stdin
|
|
3
|
+
// in Node ≥ 22: after the first line is read, stdin emits 'end', readline closes,
|
|
4
|
+
// and all subsequent question() calls resolve to "".
|
|
5
|
+
// Fix: use the async-iterator API, which buffers all lines before the close event fires.
|
|
6
|
+
let _rl = null;
|
|
7
|
+
let _lines = null;
|
|
8
|
+
function getLines() {
|
|
9
|
+
if (_lines == null) {
|
|
10
|
+
_rl = createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
terminal: false
|
|
13
|
+
});
|
|
14
|
+
_lines = _rl[Symbol.asyncIterator]();
|
|
15
|
+
}
|
|
16
|
+
return _lines;
|
|
17
|
+
}
|
|
18
|
+
/** Release stdin so the process can exit after interactive prompts. */ export function closePrompt() {
|
|
19
|
+
_rl?.close();
|
|
20
|
+
_rl = null;
|
|
21
|
+
_lines = null;
|
|
22
|
+
}
|
|
23
|
+
async function readLine(prompt) {
|
|
24
|
+
process.stdout.write(prompt);
|
|
25
|
+
const { value, done } = await getLines().next();
|
|
26
|
+
return done ? "" : value.trim();
|
|
27
|
+
}
|
|
28
|
+
export async function ask(question, fallback = "") {
|
|
29
|
+
const answer = await readLine(question);
|
|
30
|
+
return answer || fallback;
|
|
31
|
+
}
|
|
32
|
+
export async function pick(label, items, display) {
|
|
33
|
+
console.log(label.trimEnd());
|
|
34
|
+
for (const [i, item] of items.entries()){
|
|
35
|
+
console.log(` ${i + 1}. ${display(item)}`);
|
|
36
|
+
}
|
|
37
|
+
const answer = await readLine(" > ");
|
|
38
|
+
const idx = Number.parseInt(answer, 10);
|
|
39
|
+
if (Number.isNaN(idx) || idx < 1 || idx > items.length) return null;
|
|
40
|
+
return items[idx - 1];
|
|
41
|
+
}
|
package/es6/src/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { AddModule } from "./add.module.js";
|
|
2
|
+
export { AddApiAction } from "./add-api.action.js";
|
|
3
|
+
export { Auth0Module } from "./auth0.module.js";
|
|
4
|
+
export { Auth0ConfigureAction } from "./auth0-configure.action.js";
|
|
5
|
+
export { Auth0LoginAction } from "./auth0-login.action.js";
|
|
6
|
+
export { CreateModule } from "./create.module.js";
|
|
7
|
+
export { runCli } from "./program.js";
|
|
8
|
+
export { scaffoldApi } from "./webapi/create-api.js";
|
|
9
|
+
export { WebapiAction } from "./webapi.action.js";
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { isMap, isSeq, parseDocument } from "yaml";
|
|
3
|
+
import { WELL_KNOWN_FILES } from "./const.js";
|
|
4
|
+
/**
|
|
5
|
+
* Live builder for `docker-compose.yml`.
|
|
6
|
+
*
|
|
7
|
+
* Features call `addService` / `addVolume` / `addNetwork` in their scaffold phases.
|
|
8
|
+
* Loaded with `fromFile` (`decohere add api`), additions merge into the existing file:
|
|
9
|
+
* services, networks and volumes already there are left untouched, comments kept.
|
|
10
|
+
*/ export class DockerCompose {
|
|
11
|
+
key = WELL_KNOWN_FILES.DOCKER_COMPOSE;
|
|
12
|
+
_projectName;
|
|
13
|
+
_headerComments = [];
|
|
14
|
+
_services = new Map();
|
|
15
|
+
_volumes = new Set();
|
|
16
|
+
_networks = new Map();
|
|
17
|
+
_envFiles = [];
|
|
18
|
+
_existing;
|
|
19
|
+
/** Load an existing compose file so `decohere add api` can merge into it. */ static fromFile(absolutePath) {
|
|
20
|
+
const compose = new DockerCompose();
|
|
21
|
+
compose._existing = parseDocument(readFileSync(absolutePath, "utf8"));
|
|
22
|
+
return compose;
|
|
23
|
+
}
|
|
24
|
+
/** True when the service is defined (in the loaded file or added since). */ hasService(name) {
|
|
25
|
+
return this._services.has(name) || this._existing?.hasIn([
|
|
26
|
+
"services",
|
|
27
|
+
name
|
|
28
|
+
]) === true;
|
|
29
|
+
}
|
|
30
|
+
/** Compose project name (`name:` top-level) — isolates containers/volumes. */ setProjectName(name) {
|
|
31
|
+
this._projectName = name;
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
/** File-level `#` comments rendered above `name:` / `services:`. */ addHeaderComment(...lines) {
|
|
35
|
+
this._headerComments.push(...lines);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
addService(name, config) {
|
|
39
|
+
this._services.set(name, config);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
files() {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
filePath: ".devcontainer/docker-compose.yml",
|
|
46
|
+
content: this.getContent()
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
addVolume(name) {
|
|
51
|
+
this._volumes.add(name);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
addNetwork(key, network) {
|
|
55
|
+
this._networks.set(key, network);
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
/** Append an env_file path to an existing service. */ addEnvFile(serviceName, filePath) {
|
|
59
|
+
// A service already in the loaded file wins over an `addService` of the same name.
|
|
60
|
+
const svc = this._existing?.hasIn([
|
|
61
|
+
"services",
|
|
62
|
+
serviceName
|
|
63
|
+
]) ? undefined : this._services.get(serviceName);
|
|
64
|
+
if (svc) svc.env_file = [
|
|
65
|
+
...svc.env_file ?? [],
|
|
66
|
+
filePath
|
|
67
|
+
];
|
|
68
|
+
else this._envFiles.push([
|
|
69
|
+
serviceName,
|
|
70
|
+
filePath
|
|
71
|
+
]);
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
getContent() {
|
|
75
|
+
if (this._existing) return this.mergeIntoExisting(this._existing);
|
|
76
|
+
const lines = [];
|
|
77
|
+
for (const comment of this._headerComments){
|
|
78
|
+
lines.push(comment.startsWith("#") ? comment : `# ${comment}`);
|
|
79
|
+
}
|
|
80
|
+
if (this._headerComments.length > 0) {
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
if (this._projectName) {
|
|
84
|
+
lines.push(`name: ${this._projectName}`, "");
|
|
85
|
+
}
|
|
86
|
+
lines.push("services:");
|
|
87
|
+
for (const [name, svc] of this._services){
|
|
88
|
+
lines.push(` ${name}:`);
|
|
89
|
+
if (svc.build) lines.push(` build: ${svc.build}`);
|
|
90
|
+
if (svc.image) lines.push(` image: ${svc.image}`);
|
|
91
|
+
if (svc.container_name) lines.push(` container_name: ${svc.container_name}`);
|
|
92
|
+
if (svc.command) lines.push(` command: ${svc.command}`);
|
|
93
|
+
if (svc.user) lines.push(` user: "${svc.user}"`);
|
|
94
|
+
if (svc.restart) lines.push(` restart: ${svc.restart}`);
|
|
95
|
+
if (svc.profiles && svc.profiles.length > 0) {
|
|
96
|
+
lines.push(" profiles:");
|
|
97
|
+
for (const p of svc.profiles)lines.push(` - ${p}`);
|
|
98
|
+
}
|
|
99
|
+
if (svc.env_file && svc.env_file.length > 0) {
|
|
100
|
+
lines.push(" env_file:");
|
|
101
|
+
for (const f of svc.env_file)lines.push(` - ${f}`);
|
|
102
|
+
}
|
|
103
|
+
if (svc.environment && Object.keys(svc.environment).length > 0) {
|
|
104
|
+
lines.push(" environment:");
|
|
105
|
+
for (const [k, v] of Object.entries(svc.environment)){
|
|
106
|
+
lines.push(` ${k}: ${v}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (svc.ports && svc.ports.length > 0) {
|
|
110
|
+
lines.push(" ports:");
|
|
111
|
+
for (const p of svc.ports)lines.push(` - "${p}"`);
|
|
112
|
+
}
|
|
113
|
+
if (svc.volumes && svc.volumes.length > 0) {
|
|
114
|
+
lines.push(" volumes:");
|
|
115
|
+
for (const v of svc.volumes)lines.push(` - ${v}`);
|
|
116
|
+
}
|
|
117
|
+
if (svc.networks && svc.networks.length > 0) {
|
|
118
|
+
const onlyStrings = svc.networks.every((n)=>typeof n === "string");
|
|
119
|
+
if (onlyStrings) {
|
|
120
|
+
lines.push(" networks:");
|
|
121
|
+
for (const n of svc.networks)lines.push(` - ${n}`);
|
|
122
|
+
} else {
|
|
123
|
+
lines.push(" networks:");
|
|
124
|
+
for (const n of svc.networks){
|
|
125
|
+
if (typeof n === "string") {
|
|
126
|
+
lines.push(` ${n}:`);
|
|
127
|
+
} else {
|
|
128
|
+
for (const [netKey, cfg] of Object.entries(n)){
|
|
129
|
+
lines.push(` ${netKey}:`);
|
|
130
|
+
if (cfg.aliases && cfg.aliases.length > 0) {
|
|
131
|
+
lines.push(" aliases:");
|
|
132
|
+
for (const a of cfg.aliases)lines.push(` - ${a}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (svc.depends_on && svc.depends_on.length > 0) {
|
|
140
|
+
lines.push(" depends_on:");
|
|
141
|
+
for (const d of svc.depends_on)lines.push(` - ${d}`);
|
|
142
|
+
}
|
|
143
|
+
if (svc.healthcheck) {
|
|
144
|
+
const hc = svc.healthcheck;
|
|
145
|
+
lines.push(" healthcheck:");
|
|
146
|
+
lines.push(` test: ${JSON.stringify(hc.test)}`);
|
|
147
|
+
if (hc.interval) lines.push(` interval: ${hc.interval}`);
|
|
148
|
+
if (hc.timeout) lines.push(` timeout: ${hc.timeout}`);
|
|
149
|
+
if (hc.retries != null) lines.push(` retries: ${hc.retries}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (this._networks.size > 0) {
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push("networks:");
|
|
155
|
+
for (const [key, net] of this._networks){
|
|
156
|
+
lines.push(` ${key}:`);
|
|
157
|
+
lines.push(` name: ${net.name}`);
|
|
158
|
+
if (net.external) lines.push(" external: true");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (this._volumes.size > 0) {
|
|
162
|
+
lines.push("");
|
|
163
|
+
lines.push("volumes:");
|
|
164
|
+
for (const v of this._volumes)lines.push(` ${v}:`);
|
|
165
|
+
}
|
|
166
|
+
return `${lines.join("\n")}\n`;
|
|
167
|
+
}
|
|
168
|
+
mergeIntoExisting(doc) {
|
|
169
|
+
if (!isMap(doc.contents)) throw new Error("docker-compose.yml is not a YAML mapping");
|
|
170
|
+
for (const line of this._headerComments){
|
|
171
|
+
const text = line.replace(/^#\s?/, "");
|
|
172
|
+
const before = doc.commentBefore ?? "";
|
|
173
|
+
if (!before.includes(text)) doc.commentBefore = before ? `${before}\n ${text}` : ` ${text}`;
|
|
174
|
+
}
|
|
175
|
+
for (const [name, svc] of this._services){
|
|
176
|
+
if (!doc.hasIn([
|
|
177
|
+
"services",
|
|
178
|
+
name
|
|
179
|
+
])) doc.setIn([
|
|
180
|
+
"services",
|
|
181
|
+
name
|
|
182
|
+
], doc.createNode(toComposeYaml(svc)));
|
|
183
|
+
}
|
|
184
|
+
for (const [service, path] of this._envFiles){
|
|
185
|
+
if (!doc.hasIn([
|
|
186
|
+
"services",
|
|
187
|
+
service
|
|
188
|
+
])) continue;
|
|
189
|
+
const envFiles = doc.getIn([
|
|
190
|
+
"services",
|
|
191
|
+
service,
|
|
192
|
+
"env_file"
|
|
193
|
+
]);
|
|
194
|
+
if (isSeq(envFiles)) {
|
|
195
|
+
if (!envFiles.items.some((item)=>String(item) === path)) envFiles.add(path);
|
|
196
|
+
} else if (envFiles == null) {
|
|
197
|
+
doc.setIn([
|
|
198
|
+
"services",
|
|
199
|
+
service,
|
|
200
|
+
"env_file"
|
|
201
|
+
], doc.createNode([
|
|
202
|
+
path
|
|
203
|
+
]));
|
|
204
|
+
} else if (String(envFiles) !== path) {
|
|
205
|
+
doc.setIn([
|
|
206
|
+
"services",
|
|
207
|
+
service,
|
|
208
|
+
"env_file"
|
|
209
|
+
], doc.createNode([
|
|
210
|
+
String(envFiles),
|
|
211
|
+
path
|
|
212
|
+
]));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
for (const [key, network] of this._networks){
|
|
216
|
+
if (!doc.hasIn([
|
|
217
|
+
"networks",
|
|
218
|
+
key
|
|
219
|
+
])) doc.setIn([
|
|
220
|
+
"networks",
|
|
221
|
+
key
|
|
222
|
+
], doc.createNode(network));
|
|
223
|
+
}
|
|
224
|
+
for (const volume of this._volumes){
|
|
225
|
+
if (!doc.hasIn([
|
|
226
|
+
"volumes",
|
|
227
|
+
volume
|
|
228
|
+
])) doc.setIn([
|
|
229
|
+
"volumes",
|
|
230
|
+
volume
|
|
231
|
+
], null);
|
|
232
|
+
}
|
|
233
|
+
return doc.toString({
|
|
234
|
+
lineWidth: 0
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Plain compose YAML shape for a service (networks with aliases become a map). */ function toComposeYaml(svc) {
|
|
239
|
+
const { networks, ...rest } = svc;
|
|
240
|
+
if (!networks || networks.length === 0) return rest;
|
|
241
|
+
if (networks.every((n)=>typeof n === "string")) return {
|
|
242
|
+
...rest,
|
|
243
|
+
networks
|
|
244
|
+
};
|
|
245
|
+
const networkMap = {};
|
|
246
|
+
for (const network of networks){
|
|
247
|
+
if (typeof network === "string") networkMap[network] = null;
|
|
248
|
+
else Object.assign(networkMap, network);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
...rest,
|
|
252
|
+
networks: networkMap
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Builder for a YAML config file (e.g. config/config.yaml or config/config.dev.yaml).
|
|
4
|
+
*
|
|
5
|
+
* Starts with a base section passed to the constructor. Features call
|
|
6
|
+
* `addSection(yaml)` during their scaffold phase to append their own block.
|
|
7
|
+
* `files()` joins all sections with a blank line separator.
|
|
8
|
+
*/ export class ConfigYaml {
|
|
9
|
+
key;
|
|
10
|
+
filePath;
|
|
11
|
+
_sections = [];
|
|
12
|
+
constructor(key, filePath, baseContent){
|
|
13
|
+
this.key = key;
|
|
14
|
+
this.filePath = filePath;
|
|
15
|
+
this._sections.push(baseContent);
|
|
16
|
+
}
|
|
17
|
+
addSection(yaml) {
|
|
18
|
+
this._sections.push(yaml);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
/** True when a datasource with this `name:` already appears in the file. */ hasDataSource(name) {
|
|
22
|
+
return this._sections.join("\n").includes(`name: ${name}\n`);
|
|
23
|
+
}
|
|
24
|
+
hasDataSources() {
|
|
25
|
+
return this._sections.join("\n").includes("dataSources:");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Insert a `dataSources` list item. Used by `webapi add` so we do not write a
|
|
29
|
+
* second top-level `database:` key (YAML last-key-wins).
|
|
30
|
+
*/ appendDataSource(item) {
|
|
31
|
+
const current = this._sections.join("\n");
|
|
32
|
+
this._sections.length = 0;
|
|
33
|
+
this._sections.push(injectYamlDataSource(current, item));
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
static fromFile(key, relativePath, absolutePath) {
|
|
37
|
+
const content = readFileSync(absolutePath, "utf8");
|
|
38
|
+
return new ConfigYaml(key, relativePath, content);
|
|
39
|
+
}
|
|
40
|
+
files() {
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
filePath: this.filePath,
|
|
44
|
+
content: this._sections.join("\n")
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Insert a YAML list item under `dataSources:`; append a new `database:` block if missing. */ export function injectYamlDataSource(yaml, item) {
|
|
50
|
+
const block = item.replace(/\n$/, "");
|
|
51
|
+
if (!yaml.includes("dataSources:")) {
|
|
52
|
+
const prefix = yaml.trimEnd();
|
|
53
|
+
const section = `database:\n dataSources:\n${block}\n`;
|
|
54
|
+
return prefix.length > 0 ? `${prefix}\n\n${section}` : section;
|
|
55
|
+
}
|
|
56
|
+
const lines = yaml.split("\n");
|
|
57
|
+
const dsIndex = lines.findIndex((line)=>/^\s*dataSources:\s*$/.test(line));
|
|
58
|
+
if (dsIndex < 0) {
|
|
59
|
+
return `${yaml.trimEnd()}\n\ndatabase:\n dataSources:\n${block}\n`;
|
|
60
|
+
}
|
|
61
|
+
let insertAt = lines.length;
|
|
62
|
+
for(let i = dsIndex + 1; i < lines.length; i++){
|
|
63
|
+
if (/^[A-Za-z]/.test(lines[i] ?? "")) {
|
|
64
|
+
insertAt = i;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const insertLines = block.split("\n");
|
|
69
|
+
if (insertAt < lines.length && lines[insertAt - 1] !== "") {
|
|
70
|
+
insertLines.push("");
|
|
71
|
+
}
|
|
72
|
+
lines.splice(insertAt, 0, ...insertLines);
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const WELL_KNOWN_FILES = {
|
|
2
|
+
VSCODE_SETTINGS: "VSCODE_SETTINGS",
|
|
3
|
+
LAUNCH_JSON: "LAUNCH_JSON",
|
|
4
|
+
DOCKER_COMPOSE: "DOCKER_COMPOSE",
|
|
5
|
+
DEVCONTAINER_JSON: "DEVCONTAINER_JSON",
|
|
6
|
+
DOCKERFILE: "DOCKERFILE",
|
|
7
|
+
PRODUCTION_DOCKERFILE: "PRODUCTION_DOCKERFILE",
|
|
8
|
+
GITHUB_CI: "GITHUB_CI",
|
|
9
|
+
PACKAGE_JSON: "PACKAGE_JSON",
|
|
10
|
+
API_PACKAGE_JSON: "API_PACKAGE_JSON",
|
|
11
|
+
TSCONFIG: "TSCONFIG",
|
|
12
|
+
VITEST_CONFIG: "VITEST_CONFIG",
|
|
13
|
+
MAIN_TS: "MAIN_TS",
|
|
14
|
+
CONTROLLER_TS: "CONTROLLER_TS",
|
|
15
|
+
CONFIG_YAML: "CONFIG_YAML",
|
|
16
|
+
CONFIG_DEV_YAML: "CONFIG_DEV_YAML",
|
|
17
|
+
GITIGNORE: "GITIGNORE",
|
|
18
|
+
AUTH_CONFIG: "AUTH_CONFIG",
|
|
19
|
+
ENV_FILE: "ENV_FILE"
|
|
20
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { WELL_KNOWN_FILES } from "./const.js";
|
|
3
|
+
export class DevContainer {
|
|
4
|
+
key = WELL_KNOWN_FILES.DEVCONTAINER_JSON;
|
|
5
|
+
_extensions = [];
|
|
6
|
+
_ports = [];
|
|
7
|
+
_portAttributes = new Map();
|
|
8
|
+
_mounts;
|
|
9
|
+
_workspaceFolder = "/workspace";
|
|
10
|
+
_postCreateCommand;
|
|
11
|
+
_updateContentCommand;
|
|
12
|
+
_postStartCommand;
|
|
13
|
+
_initializeCommand;
|
|
14
|
+
_remoteUser = "node";
|
|
15
|
+
_useCompose = false;
|
|
16
|
+
_existing;
|
|
17
|
+
/**
|
|
18
|
+
* Load an existing devcontainer.json so `decohere add api` can merge into it.
|
|
19
|
+
* Only extensions, forwarded ports and port attributes are merged; everything else is kept.
|
|
20
|
+
*/ static fromFile(absolutePath) {
|
|
21
|
+
const devContainer = new DevContainer();
|
|
22
|
+
try {
|
|
23
|
+
devContainer._existing = JSON.parse(readFileSync(absolutePath, "utf8"));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`Can't update ${absolutePath}: it isn't plain JSON (${error.message}). Remove any comments or trailing commas and try again.`);
|
|
26
|
+
}
|
|
27
|
+
return devContainer;
|
|
28
|
+
}
|
|
29
|
+
constructor(){
|
|
30
|
+
this._mounts = [
|
|
31
|
+
this.bindMount(this._workspaceFolder)
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
/** In-container path Cursor/VS Code opens (Agents window uses the basename). */ setWorkspaceFolder(folder) {
|
|
35
|
+
this._workspaceFolder = folder;
|
|
36
|
+
this._mounts = [
|
|
37
|
+
this.bindMount(folder)
|
|
38
|
+
];
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
bindMount(target) {
|
|
42
|
+
return `source=..,target=${target},type=bind,consistency=cached`;
|
|
43
|
+
}
|
|
44
|
+
addExtension(...ids) {
|
|
45
|
+
this._extensions.push(...ids);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
forwardPort(...ports) {
|
|
49
|
+
this._ports.push(...ports);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
setPortAttributes(port, attrs) {
|
|
53
|
+
this._portAttributes.set(port, attrs);
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
setPostCreateCommand(cmd) {
|
|
57
|
+
this._postCreateCommand = cmd;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
setUpdateContentCommand(cmd) {
|
|
61
|
+
this._updateContentCommand = cmd;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
setPostStartCommand(cmd) {
|
|
65
|
+
this._postStartCommand = cmd;
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
setInitializeCommand(cmd) {
|
|
69
|
+
this._initializeCommand = cmd;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
setRemoteUser(user) {
|
|
73
|
+
this._remoteUser = user;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
useCompose() {
|
|
77
|
+
this._useCompose = true;
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
files() {
|
|
81
|
+
const config = this._existing ? this.mergeIntoExisting(this._existing) : this.build();
|
|
82
|
+
return [
|
|
83
|
+
{
|
|
84
|
+
filePath: ".devcontainer/devcontainer.json",
|
|
85
|
+
content: JSON.stringify(config, null, 2)
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
mergeIntoExisting(existing) {
|
|
90
|
+
const config = structuredClone(existing);
|
|
91
|
+
if (this._extensions.length > 0) {
|
|
92
|
+
const customizations = objectAt(config, "customizations");
|
|
93
|
+
const vscode = objectAt(customizations, "vscode");
|
|
94
|
+
vscode.extensions = union(vscode.extensions, this._extensions);
|
|
95
|
+
}
|
|
96
|
+
if (this._ports.length > 0) {
|
|
97
|
+
config.forwardPorts = union(config.forwardPorts, this._ports);
|
|
98
|
+
}
|
|
99
|
+
if (this._portAttributes.size > 0) {
|
|
100
|
+
const attributes = objectAt(config, "portsAttributes");
|
|
101
|
+
for (const [port, attrs] of this._portAttributes){
|
|
102
|
+
attributes[String(port)] ??= attrs;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
build() {
|
|
108
|
+
const config = {};
|
|
109
|
+
if (this._useCompose) {
|
|
110
|
+
config.dockerComposeFile = "docker-compose.yml";
|
|
111
|
+
config.service = "devcontainer";
|
|
112
|
+
config.workspaceFolder = this._workspaceFolder;
|
|
113
|
+
}
|
|
114
|
+
if (this._initializeCommand) {
|
|
115
|
+
config.initializeCommand = this._initializeCommand;
|
|
116
|
+
}
|
|
117
|
+
if (this._mounts) {
|
|
118
|
+
config.mounts = this._mounts;
|
|
119
|
+
}
|
|
120
|
+
if (this._extensions.length > 0) {
|
|
121
|
+
config.customizations = {
|
|
122
|
+
vscode: {
|
|
123
|
+
extensions: this._extensions
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (this._ports.length > 0) {
|
|
128
|
+
config.forwardPorts = this._ports;
|
|
129
|
+
}
|
|
130
|
+
if (this._portAttributes.size > 0) {
|
|
131
|
+
const portsAttributes = {};
|
|
132
|
+
for (const [port, attrs] of this._portAttributes){
|
|
133
|
+
portsAttributes[String(port)] = attrs;
|
|
134
|
+
}
|
|
135
|
+
config.portsAttributes = portsAttributes;
|
|
136
|
+
}
|
|
137
|
+
if (this._postCreateCommand) {
|
|
138
|
+
config.postCreateCommand = this._postCreateCommand;
|
|
139
|
+
}
|
|
140
|
+
if (this._updateContentCommand) {
|
|
141
|
+
config.updateContentCommand = this._updateContentCommand;
|
|
142
|
+
}
|
|
143
|
+
if (this._postStartCommand) {
|
|
144
|
+
config.postStartCommand = this._postStartCommand;
|
|
145
|
+
}
|
|
146
|
+
if (this._remoteUser) {
|
|
147
|
+
config.remoteUser = this._remoteUser;
|
|
148
|
+
}
|
|
149
|
+
return config;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** The object at `parent[key]`, created when missing. */ function objectAt(parent, key) {
|
|
153
|
+
parent[key] ??= {};
|
|
154
|
+
return parent[key];
|
|
155
|
+
}
|
|
156
|
+
function union(existing, additions) {
|
|
157
|
+
const items = Array.isArray(existing) ? [
|
|
158
|
+
...existing
|
|
159
|
+
] : [];
|
|
160
|
+
for (const item of additions)if (!items.includes(item)) items.push(item);
|
|
161
|
+
return items;
|
|
162
|
+
}
|