@decohere/scaffold 0.0.1-dev.18 → 0.0.1-dev.20261105.54
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/LICENSE +15 -0
- 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 +8 -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 +157 -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 +275 -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 +433 -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 +238 -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 +4 -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 +142 -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 +50 -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 +255 -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 +423 -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/{src/webapi/models/github-ci.model.ts → es6/src/webapi/models/github-ci.model.js} +27 -29
- 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 +179 -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 +19 -9
- 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/{src/index.ts → types/src/index.d.ts} +4 -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/{src/models/source-file.ts → types/src/models/source-file.d.ts} +4 -6
- 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 +8 -0
- package/{src/resolve-target-dir.ts → types/src/resolve-target-dir.d.ts} +1 -4
- 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/{src/webapi/const.ts → types/src/webapi/const.d.ts} +9 -21
- 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
- package/src/add-api.action.ts +0 -25
- package/src/add-api.command.ts +0 -31
- package/src/add.command.ts +0 -20
- package/src/add.module.ts +0 -11
- package/src/cli.ts +0 -4
- package/src/create.command.ts +0 -20
- package/src/create.module.ts +0 -11
- package/src/domain/prompt.ts +0 -50
- package/src/models/compose.model.ts +0 -205
- package/src/models/config-yaml.model.ts +0 -83
- package/src/models/const.ts +0 -20
- package/src/models/dev-container.model.ts +0 -136
- package/src/models/docker-image.model.ts +0 -343
- package/src/models/env-file.model.ts +0 -45
- package/src/models/gitignore.model.ts +0 -35
- package/src/models/launch-json.model.ts +0 -60
- package/src/models/package-json.model.ts +0 -169
- package/src/models/project.model.ts +0 -141
- package/src/models/tsconfig.model.ts +0 -29
- package/src/models/vitest-config.model.ts +0 -26
- package/src/models/vscode-settings.model.ts +0 -24
- package/src/program.ts +0 -63
- package/src/services/scaffold.service.ts +0 -89
- package/src/version.ts +0 -56
- package/src/webapi/api-name.ts +0 -73
- package/src/webapi/cli-options.ts +0 -56
- package/src/webapi/create-api.ts +0 -275
- package/src/webapi/devcontainer/host-scripts.ts +0 -35
- package/src/webapi/features/auth.feature.ts +0 -454
- package/src/webapi/features/auth0-cli.ts +0 -421
- package/src/webapi/features/biome-format.ts +0 -34
- package/src/webapi/features/database.feature.ts +0 -522
- package/src/webapi/features/finalize.feature.ts +0 -112
- package/src/webapi/features/postman.feature.ts +0 -287
- package/src/webapi/models/api-package-json.model.ts +0 -60
- package/src/webapi/models/auth-config.model.ts +0 -36
- package/src/webapi/models/controller-source-file.model.ts +0 -232
- package/src/webapi/models/main-source-file.model.ts +0 -208
- package/src/webapi/models/production-dockerfile.model.ts +0 -162
- package/src/webapi/models/sample-files.model.ts +0 -45
- package/src/webapi/models/webapi-image.model.ts +0 -21
- package/src/webapi/postman/hello-openapi.ts +0 -92
- package/src/webapi/postman/local-view.ts +0 -195
- package/src/webapi/postman/openapi-to-postman.ts +0 -208
- package/src/webapi/workspace-readme.ts +0 -192
- package/src/webapi.action.ts +0 -22
- package/src/webapi.command.ts +0 -41
- package/tests/auth-postman.wiring.spec.ts +0 -263
- package/tests/auth0-cli.spec.ts +0 -232
- package/tests/biome-format.spec.ts +0 -49
- package/tests/cli.integration.spec.ts +0 -658
- package/tests/compose.model.spec.ts +0 -51
- package/tests/container-image.model.spec.ts +0 -162
- package/tests/dev-container.model.spec.ts +0 -29
- package/tests/devcontainer-base-image.spec.ts +0 -32
- package/tests/hello-di-inject.scaffold.spec.ts +0 -18
- package/tests/hello-route.contract.spec.ts +0 -94
- package/tests/package-json.order.spec.ts +0 -59
- package/tests/postman-from-openapi.spec.ts +0 -197
- package/tests/seed-devcontainer-templates.ts +0 -40
- package/tests/webapi/devcontainer/codebase-rag-mcp.spec.ts +0 -96
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* A sub-unit of the monorepo (e.g. `projects/api`).
|
|
5
|
+
*
|
|
6
|
+
* Models and files registered here produce paths relative to `dir`. The parent
|
|
7
|
+
* `MonoRepoScaffold` calls `files()` and prepends `dir/` to every path, so
|
|
8
|
+
* models remain unaware of where they live in the repo.
|
|
9
|
+
*/ export class ProjectScaffold {
|
|
10
|
+
dir;
|
|
11
|
+
_models = new Map();
|
|
12
|
+
_files = [];
|
|
13
|
+
constructor(dir){
|
|
14
|
+
this.dir = dir;
|
|
15
|
+
}
|
|
16
|
+
addModel(model) {
|
|
17
|
+
this._models.set(model.key, model);
|
|
18
|
+
return model;
|
|
19
|
+
}
|
|
20
|
+
addFile(provider) {
|
|
21
|
+
this._files.push(...provider.files());
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
getModel(key) {
|
|
25
|
+
return this._models.get(key);
|
|
26
|
+
}
|
|
27
|
+
getModels(ctor) {
|
|
28
|
+
return [
|
|
29
|
+
...this._models.values()
|
|
30
|
+
].filter((m)=>m instanceof ctor);
|
|
31
|
+
}
|
|
32
|
+
/** Returns all files with `dir/` prepended to each path. */ files() {
|
|
33
|
+
const all = [
|
|
34
|
+
...this._files,
|
|
35
|
+
...[
|
|
36
|
+
...this._models.values()
|
|
37
|
+
].flatMap((m)=>m.files())
|
|
38
|
+
];
|
|
39
|
+
return all.map((f)=>({
|
|
40
|
+
filePath: `${this.dir}/${f.filePath}`,
|
|
41
|
+
content: f.content
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export class WorkspaceScaffold {
|
|
46
|
+
files = [];
|
|
47
|
+
targetDir = process.cwd();
|
|
48
|
+
_name;
|
|
49
|
+
/** Project name — host directory basename, not the in-container mount path. */ get name() {
|
|
50
|
+
return this._name ?? basename(this.targetDir);
|
|
51
|
+
}
|
|
52
|
+
setName(name) {
|
|
53
|
+
this._name = name;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
/** Add an ad-hoc file provider whose output is included at render time. */ addFile(provider) {
|
|
57
|
+
this.files.push(...provider.files());
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class MonoRepoScaffold extends WorkspaceScaffold {
|
|
62
|
+
_models = new Map();
|
|
63
|
+
_projects = [];
|
|
64
|
+
/** Register a named project. */ addProject(project) {
|
|
65
|
+
this._projects.push(project);
|
|
66
|
+
return project;
|
|
67
|
+
}
|
|
68
|
+
/** Retrieve a previously-registered project by its directory path. */ getProject(dir) {
|
|
69
|
+
return this._projects.find((p)=>p.dir === dir);
|
|
70
|
+
}
|
|
71
|
+
/** All registered projects, in registration order. */ getProjects() {
|
|
72
|
+
return this._projects;
|
|
73
|
+
}
|
|
74
|
+
/** Register a named model at workspace level. */ addModel(model) {
|
|
75
|
+
this._models.set(model.key, model);
|
|
76
|
+
return model;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Retrieve a model by key. Searches workspace models first, then all
|
|
80
|
+
* registered projects in order.
|
|
81
|
+
*/ getModel(key) {
|
|
82
|
+
const m = this._models.get(key);
|
|
83
|
+
if (m !== undefined) return m;
|
|
84
|
+
for (const project of this._projects){
|
|
85
|
+
const pm = project.getModel(key);
|
|
86
|
+
if (pm !== undefined) return pm;
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Retrieve all models of a given type. Searches workspace models and all
|
|
92
|
+
* projects, so features can discover controllers across multiple projects.
|
|
93
|
+
*/ getModels(ctor) {
|
|
94
|
+
return [
|
|
95
|
+
...[
|
|
96
|
+
...this._models.values()
|
|
97
|
+
].filter((m)=>m instanceof ctor),
|
|
98
|
+
...this._projects.flatMap((p)=>p.getModels(ctor))
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
/** Render all models and projects into files, then write everything to disk. */ async scaffold() {
|
|
102
|
+
const allFiles = [
|
|
103
|
+
...this.files,
|
|
104
|
+
...[
|
|
105
|
+
...this._models.values()
|
|
106
|
+
].flatMap((m)=>m.files()),
|
|
107
|
+
...this._projects.flatMap((p)=>p.files())
|
|
108
|
+
];
|
|
109
|
+
await Promise.all(allFiles.map(async (file)=>{
|
|
110
|
+
const fullPath = join(this.targetDir, file.filePath);
|
|
111
|
+
await mkdir(dirname(fullPath), {
|
|
112
|
+
recursive: true
|
|
113
|
+
});
|
|
114
|
+
await writeFile(fullPath, file.content);
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { WELL_KNOWN_FILES } from "./const.js";
|
|
2
|
+
export class TsConfig {
|
|
3
|
+
key = WELL_KNOWN_FILES.TSCONFIG;
|
|
4
|
+
files() {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
filePath: "tsconfig.json",
|
|
8
|
+
content: JSON.stringify({
|
|
9
|
+
compilerOptions: {
|
|
10
|
+
target: "ES2022",
|
|
11
|
+
module: "NodeNext",
|
|
12
|
+
moduleResolution: "NodeNext",
|
|
13
|
+
strict: true,
|
|
14
|
+
skipLibCheck: true,
|
|
15
|
+
experimentalDecorators: true,
|
|
16
|
+
emitDecoratorMetadata: true
|
|
17
|
+
}
|
|
18
|
+
}, null, 2)
|
|
19
|
+
}
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { WELL_KNOWN_FILES } from "./const.js";
|
|
2
|
+
export class VitestConfig {
|
|
3
|
+
key = WELL_KNOWN_FILES.VITEST_CONFIG;
|
|
4
|
+
files() {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
filePath: "vitest.config.ts",
|
|
8
|
+
content: [
|
|
9
|
+
'import { forgeBuild } from "@decohere/forge/vitest";',
|
|
10
|
+
'import { defineConfig } from "vitest/config";',
|
|
11
|
+
"",
|
|
12
|
+
"export default defineConfig({",
|
|
13
|
+
'\tplugins: [forgeBuild({ logLevel: "silent" })],',
|
|
14
|
+
"\ttest: {",
|
|
15
|
+
'\t\tinclude: ["projects/**/*.{test,spec}.?(c|m)[jt]s?(x)"],',
|
|
16
|
+
"\t},",
|
|
17
|
+
"});",
|
|
18
|
+
""
|
|
19
|
+
].join("\n")
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { WELL_KNOWN_FILES } from "./const.js";
|
|
2
|
+
export class VSCodeSettings {
|
|
3
|
+
title;
|
|
4
|
+
key = WELL_KNOWN_FILES.VSCODE_SETTINGS;
|
|
5
|
+
constructor(title){
|
|
6
|
+
this.title = title;
|
|
7
|
+
}
|
|
8
|
+
files() {
|
|
9
|
+
return [
|
|
10
|
+
{
|
|
11
|
+
filePath: ".vscode/settings.json",
|
|
12
|
+
content: JSON.stringify({
|
|
13
|
+
"window.title": `${this.title}\${separator}\${activeEditorShort}`,
|
|
14
|
+
"window.titleSeparator": " | "
|
|
15
|
+
}, null, 2)
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Command, CommanderError } from "commander";
|
|
2
|
+
import { CreateCliError } from "./webapi/api-name.js";
|
|
3
|
+
import { runAuth0Configure, runAuth0Login } from "./webapi/auth0-commands.js";
|
|
4
|
+
import { addWebapiFeatureOptions, collectName, runScaffoldApi, toApiOptions } from "./webapi/cli-options.js";
|
|
5
|
+
export function createProgram() {
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program.exitOverride();
|
|
8
|
+
program.name("decohere").description("Decohere CLI").option("-d <directory>", "Target directory (default: current directory)", ".");
|
|
9
|
+
const create = program.command("create").description("Create a new Decohere workspace");
|
|
10
|
+
addWebapiFeatureOptions(create.command("webapi").argument("[directory]", "Target directory (default: -d or current directory)").option("--name <slug>", "API name under projects/ (repeatable; default: api)", collectName, [])).action(async (directory, _opts, cmd)=>{
|
|
11
|
+
await runScaffoldApi(toApiOptions(directory, cmd.optsWithGlobals()));
|
|
12
|
+
});
|
|
13
|
+
addWebapiFeatureOptions(program.command("add").description("Add to an existing Decohere workspace").command("api").argument("<name>", "API name under projects/")).action(async (name, _opts, cmd)=>{
|
|
14
|
+
await runScaffoldApi(toApiOptions(undefined, cmd.optsWithGlobals(), {
|
|
15
|
+
names: [
|
|
16
|
+
name
|
|
17
|
+
],
|
|
18
|
+
add: true
|
|
19
|
+
}));
|
|
20
|
+
});
|
|
21
|
+
const auth0 = program.command("auth0").description("Log in to Auth0 and update a scaffolded client");
|
|
22
|
+
auth0.command("login").description("Authenticate the Auth0 CLI (device URL; tokens persist on the host)").action(()=>{
|
|
23
|
+
runAuth0Login();
|
|
24
|
+
});
|
|
25
|
+
auth0.command("configure").description("Update Auth0 API permissions and Postman SPA from @Authorize in this workspace").action((_opts, cmd)=>{
|
|
26
|
+
const d = cmd.optsWithGlobals().d;
|
|
27
|
+
runAuth0Configure(typeof d === "string" ? d : ".");
|
|
28
|
+
});
|
|
29
|
+
return program;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Runs the CLI and returns its exit code; the bin (`cli.ts`) exits with it. Returning instead
|
|
33
|
+
* of calling `process.exit` here lets tests run it in process: a run that finished after its
|
|
34
|
+
* test had timed out used to call `process.exit` in whichever test came next.
|
|
35
|
+
*/ export async function runCli(argv = process.argv) {
|
|
36
|
+
try {
|
|
37
|
+
await createProgram().parseAsync(argv);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err instanceof CommanderError) {
|
|
40
|
+
return err.exitCode;
|
|
41
|
+
}
|
|
42
|
+
if (err instanceof CreateCliError) {
|
|
43
|
+
console.error(`\n ${err.message}\n`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
console.error(err);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
50
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve scaffold destination: positional `webapi my-app` wins over `-d`.
|
|
3
|
+
* `npm create … webapi -d my-app` drops `-d` (npm loglevel) and forwards `my-app`.
|
|
4
|
+
*/ export function resolveTargetDir(positional, flag) {
|
|
5
|
+
const dir = positional?.trim() || flag?.trim() || ".";
|
|
6
|
+
return dir;
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
async function scaffold(workspace, cliOptions, features) {
|
|
2
|
+
const options = await getAllOptions(workspace, cliOptions, features);
|
|
3
|
+
// Features mutate workspace models first, then workspace renders to disk.
|
|
4
|
+
await scaffoldFeatures(workspace, options, features);
|
|
5
|
+
await scaffoldWorkspace(workspace, options);
|
|
6
|
+
// Post-write phase: install deps, print final messages, etc.
|
|
7
|
+
await finalizeFeatures(workspace, options, features);
|
|
8
|
+
}
|
|
9
|
+
async function scaffoldWorkspace(workspace, _options) {
|
|
10
|
+
await workspace.scaffold();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Iterate features in order, calling `getOptions` on each.
|
|
14
|
+
* Each feature receives the running merged options so later features can
|
|
15
|
+
* branch on decisions made by earlier ones (e.g. database choice affects auth).
|
|
16
|
+
* Results are shallow-merged, so features only need to return what they own.
|
|
17
|
+
*/ async function getAllOptions(workspace, cliOptions, features) {
|
|
18
|
+
let options = {
|
|
19
|
+
...cliOptions
|
|
20
|
+
};
|
|
21
|
+
for (const feature of features){
|
|
22
|
+
const featureOptions = await feature.getOptions(workspace, options);
|
|
23
|
+
options = {
|
|
24
|
+
...options,
|
|
25
|
+
...featureOptions
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
return options;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Iterate features in order, calling `scaffold` with the fully-resolved options.
|
|
32
|
+
* All option-gathering is complete before any scaffolding begins.
|
|
33
|
+
*/ async function scaffoldFeatures(workspace, options, features) {
|
|
34
|
+
for (const feature of features){
|
|
35
|
+
await feature.scaffold(workspace, options);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Iterate features in order, calling `finalize` on each (if defined).
|
|
40
|
+
* All files are on disk by this point.
|
|
41
|
+
*/ async function finalizeFeatures(workspace, options, features) {
|
|
42
|
+
for (const feature of features){
|
|
43
|
+
if (feature.finalize) {
|
|
44
|
+
await feature.finalize(workspace, options);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export const ScaffoldService = {
|
|
49
|
+
scaffold
|
|
50
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const _require = createRequire(import.meta.url);
|
|
5
|
+
const DEFAULT_CREATE_VERSION = "0.0.1-dev";
|
|
6
|
+
const DEFAULT_CREATE_IMAGE_REPOSITORY = "ghcr.io/typesmiths/decohere/create";
|
|
7
|
+
const DEFAULT_DEVCONTAINER_IMAGE_REPOSITORY = "ghcr.io/typesmiths/decohere/devcontainer";
|
|
8
|
+
function resolveCreateMetadata() {
|
|
9
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
for(let i = 0; i < 5; i++){
|
|
11
|
+
try {
|
|
12
|
+
const pkg = _require(join(dir, "package.json"));
|
|
13
|
+
if (pkg.name === "@decohere/scaffold") {
|
|
14
|
+
return {
|
|
15
|
+
version: pkg.version ?? DEFAULT_CREATE_VERSION,
|
|
16
|
+
imageRepository: pkg.decohere?.dockerImageRepository ?? DEFAULT_CREATE_IMAGE_REPOSITORY,
|
|
17
|
+
devcontainerImageRepository: pkg.decohere?.devcontainerImageRepository ?? DEFAULT_DEVCONTAINER_IMAGE_REPOSITORY
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
} catch {}
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir) break;
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
version: DEFAULT_CREATE_VERSION,
|
|
27
|
+
imageRepository: DEFAULT_CREATE_IMAGE_REPOSITORY,
|
|
28
|
+
devcontainerImageRepository: DEFAULT_DEVCONTAINER_IMAGE_REPOSITORY
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const createMetadata = resolveCreateMetadata();
|
|
32
|
+
/** Version of @decohere/create, used to pin scaffolded @decohere/* dependencies. */ export const CREATE_VERSION = createMetadata.version;
|
|
33
|
+
/** Canonical published Docker repository for the @decohere/create builder image. */ export const CREATE_IMAGE_REPOSITORY = createMetadata.imageRepository;
|
|
34
|
+
/** Published app Dev Container base (`docker pull` updates shared layers). */ export const DEVCONTAINER_IMAGE_REPOSITORY = createMetadata.devcontainerImageRepository;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { DEFAULT_WEBAPI_NAME } from "./const.js";
|
|
4
|
+
/** Lowercase npm-safe directory slug: `billing`, `admin-api`. */ export const API_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
5
|
+
export class CreateCliError extends Error {
|
|
6
|
+
constructor(message){
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "CreateCliError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function assertValidApiName(name) {
|
|
12
|
+
if (!API_NAME_PATTERN.test(name)) {
|
|
13
|
+
throw new CreateCliError(`Invalid API name "${name}". Use a lowercase slug starting with a letter, then letters, digits, or hyphens (e.g. billing).`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Default `["api"]` when the caller omitted `--name`. Rejects duplicates and invalid slugs. */ export function parseApiNames(raw) {
|
|
17
|
+
const names = raw != null && raw.length > 0 ? [
|
|
18
|
+
...raw
|
|
19
|
+
] : [
|
|
20
|
+
DEFAULT_WEBAPI_NAME
|
|
21
|
+
];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const name of names){
|
|
24
|
+
assertValidApiName(name);
|
|
25
|
+
if (seen.has(name)) {
|
|
26
|
+
throw new CreateCliError(`Duplicate API name "${name}".`);
|
|
27
|
+
}
|
|
28
|
+
seen.add(name);
|
|
29
|
+
}
|
|
30
|
+
return names;
|
|
31
|
+
}
|
|
32
|
+
export function isDecohereWorkspace(dir) {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
35
|
+
const build = pkg.scripts?.build;
|
|
36
|
+
return typeof build === "string" && build.includes("forge");
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Directory names under `projects/` that already have a `package.json`. */ export function listExistingApiNames(dir) {
|
|
42
|
+
const projectsDir = join(dir, "projects");
|
|
43
|
+
try {
|
|
44
|
+
return readdirSync(projectsDir, {
|
|
45
|
+
withFileTypes: true
|
|
46
|
+
}).filter((entry)=>entry.isDirectory() && existsSync(join(projectsDir, entry.name, "package.json"))).map((entry)=>entry.name).sort();
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** `billing` → `BILLING`, `admin-api` → `ADMIN_API` — prefix for `{PREFIX}_DB_HOST`. */ export function apiEnvPrefix(apiName) {
|
|
52
|
+
return apiName.replace(/-/g, "_").toUpperCase();
|
|
53
|
+
}
|
|
54
|
+
export function apiNameFromProjectDir(dir) {
|
|
55
|
+
const slash = dir.lastIndexOf("/");
|
|
56
|
+
return slash >= 0 ? dir.slice(slash + 1) : dir;
|
|
57
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { CreateCliError, isDecohereWorkspace } from "./api-name.js";
|
|
4
|
+
import { discoverAuthorizeScopes, mergeApiScopes, postmanRequestedScopes } from "./authorize-scopes.js";
|
|
5
|
+
import * as auth0Cli from "./features/auth0-cli.js";
|
|
6
|
+
/** Same Postman OAuth callbacks as `PostmanFeature`. */ const POSTMAN_CALLBACKS = [
|
|
7
|
+
"https://oauth.pstmn.io/v1/callback",
|
|
8
|
+
"https://oauth.pstmn.io/v1/browser-callback"
|
|
9
|
+
];
|
|
10
|
+
const SPA_SUFFIX = " Postman";
|
|
11
|
+
export const AUTH0_CLI_INSTALL_MESSAGE = "The Auth0 CLI is required.\n" + " It is bundled in the Decohere create image (`npx @decohere/create auth0 login`).\n" + " Host install: brew install auth0/auth0-cli/auth0\n" + " Or visit: https://github.com/auth0/auth0-cli";
|
|
12
|
+
function fail(message) {
|
|
13
|
+
throw new CreateCliError(message);
|
|
14
|
+
}
|
|
15
|
+
/** Throw if the `auth0` binary is missing. */ export function requireInstalled() {
|
|
16
|
+
if (auth0Cli.isInstalled()) return;
|
|
17
|
+
fail(AUTH0_CLI_INSTALL_MESSAGE);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Log in when there is no tenant session or the Management API token is expired.
|
|
21
|
+
* Returns the active tenant.
|
|
22
|
+
*/ export function ensureLoggedIn() {
|
|
23
|
+
requireInstalled();
|
|
24
|
+
process.stdout.write("\n Checking Auth0 credentials...");
|
|
25
|
+
let tenants = auth0Cli.listTenants();
|
|
26
|
+
if (tenants.length === 0) {
|
|
27
|
+
process.stdout.write(" not logged in.\n");
|
|
28
|
+
console.log(" Logging in to Auth0 (open the URL in your browser)...");
|
|
29
|
+
if (!auth0Cli.login()) fail("Auth0 login failed.");
|
|
30
|
+
tenants = auth0Cli.listTenants();
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
auth0Cli.listApis();
|
|
34
|
+
process.stdout.write(" ok.\n");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err instanceof auth0Cli.Auth0AuthError) {
|
|
37
|
+
process.stdout.write(" token expired.\n");
|
|
38
|
+
console.log(" Re-authenticating (open the URL in your browser)...");
|
|
39
|
+
if (!auth0Cli.login()) fail("Auth0 login failed.");
|
|
40
|
+
tenants = auth0Cli.listTenants();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const active = tenants.find((t)=>t.active) ?? tenants[0];
|
|
44
|
+
if (active == null) fail("No Auth0 tenant after login.");
|
|
45
|
+
return active;
|
|
46
|
+
}
|
|
47
|
+
export function runAuth0Login() {
|
|
48
|
+
requireInstalled();
|
|
49
|
+
console.log(" Logging in to Auth0 (open the URL in your browser)...");
|
|
50
|
+
if (!auth0Cli.login()) fail("Auth0 login failed.");
|
|
51
|
+
const tenants = auth0Cli.listTenants();
|
|
52
|
+
const active = tenants.find((t)=>t.active) ?? tenants[0];
|
|
53
|
+
if (active == null) fail("No Auth0 tenant after login.");
|
|
54
|
+
console.log(`\n ✓ Auth0 tenant: https://${active.name}`);
|
|
55
|
+
console.log(" Tokens are stored in ~/.config/auth0 (mounted into the create image).");
|
|
56
|
+
console.log(" Next: npx @decohere/create auth0 configure");
|
|
57
|
+
}
|
|
58
|
+
export function runAuth0Configure(directory = ".") {
|
|
59
|
+
const targetDir = resolve(directory);
|
|
60
|
+
if (!isDecohereWorkspace(targetDir)) {
|
|
61
|
+
throw new CreateCliError(`Not a Decohere workspace: ${targetDir}. Scaffold one first with \`npx @decohere/create webapi\`.`);
|
|
62
|
+
}
|
|
63
|
+
const active = ensureLoggedIn();
|
|
64
|
+
const issuerBaseUrl = `https://${active.name}`;
|
|
65
|
+
const projectName = process.env.DECOHERE_PROJECT_NAME?.trim() || basename(targetDir);
|
|
66
|
+
const envPath = join(targetDir, ".env");
|
|
67
|
+
const existing = readEnvFile(envPath);
|
|
68
|
+
const existingAudience = existing.AUTH0_AUDIENCE?.trim();
|
|
69
|
+
const discovered = discoverAuthorizeScopes(targetDir);
|
|
70
|
+
let existingScopes = [];
|
|
71
|
+
if (existingAudience) {
|
|
72
|
+
try {
|
|
73
|
+
existingScopes = auth0Cli.listApiScopes(existingAudience);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err instanceof auth0Cli.Auth0AuthError) throw err;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const apiScopes = resolveApiScopes(discovered, existingScopes);
|
|
79
|
+
console.log(discovered.length > 0 ? ` ✓ Scanned @Authorize — ${discovered.length} scope(s) from controllers` : " ✓ No @Authorize in projects/*/src — keeping existing / Hello scopes");
|
|
80
|
+
let audience = existingAudience;
|
|
81
|
+
if (audience) {
|
|
82
|
+
try {
|
|
83
|
+
auth0Cli.updateApiScopes(audience, apiScopes);
|
|
84
|
+
console.log(` ✓ API scopes ensured (${apiScopes.join(", ")})`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.error(` ✗ Failed to update API scopes: ${err instanceof Error ? err.message : err}`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const identifier = `https://api.${projectName}`;
|
|
90
|
+
console.log(`\n Creating Auth0 API ${identifier}...`);
|
|
91
|
+
audience = auth0Cli.createApi(`${projectName} API`, identifier, apiScopes).identifier;
|
|
92
|
+
console.log(` ✓ API created: ${audience}`);
|
|
93
|
+
}
|
|
94
|
+
const appName = `${projectName}${SPA_SUFFIX}`;
|
|
95
|
+
const app = auth0Cli.ensureSpaApp(appName, [
|
|
96
|
+
...POSTMAN_CALLBACKS
|
|
97
|
+
]);
|
|
98
|
+
console.log(app.reused ? ` ✓ Reusing Auth0 SPA app (${app.client_id})` : ` ✓ Auth0 SPA app created (${app.client_id})`);
|
|
99
|
+
try {
|
|
100
|
+
auth0Cli.enableConnectionForClient(auth0Cli.DB_CONNECTION_NAME, app.client_id);
|
|
101
|
+
console.log(` ✓ Enabled ${auth0Cli.DB_CONNECTION_NAME} on SPA`);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.error(` ✗ Failed to enable DB connection on SPA: ${err instanceof Error ? err.message : err}`);
|
|
104
|
+
}
|
|
105
|
+
upsertEnv(envPath, {
|
|
106
|
+
AUTH0_ISSUER_BASE_URL: issuerBaseUrl,
|
|
107
|
+
AUTH0_AUDIENCE: audience
|
|
108
|
+
});
|
|
109
|
+
console.log(` ✓ Wrote AUTH0_ISSUER_BASE_URL and AUTH0_AUDIENCE to .env`);
|
|
110
|
+
const adminSub = existing.AUTH0_ADMIN_USER_SUB?.trim();
|
|
111
|
+
if (adminSub) {
|
|
112
|
+
try {
|
|
113
|
+
auth0Cli.assignUserPermissions(adminSub, audience, apiScopes);
|
|
114
|
+
console.log(` ✓ Assigned ${apiScopes.length} API permission(s) to admin user`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(` ✗ Failed to assign admin permissions: ${err instanceof Error ? err.message : err}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const domain = issuerBaseUrl.replace(/^https?:\/\//, "");
|
|
120
|
+
patchPostmanAuth(targetDir, projectName, {
|
|
121
|
+
domain,
|
|
122
|
+
audience,
|
|
123
|
+
clientId: app.client_id,
|
|
124
|
+
scopes: postmanRequestedScopes(apiScopes)
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function readEnvFile(path) {
|
|
128
|
+
if (!existsSync(path)) return {};
|
|
129
|
+
const out = {};
|
|
130
|
+
for (const line of readFileSync(path, "utf8").split("\n")){
|
|
131
|
+
const trimmed = line.trim();
|
|
132
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
133
|
+
const eq = line.indexOf("=");
|
|
134
|
+
if (eq <= 0) continue;
|
|
135
|
+
out[line.slice(0, eq).trim()] = line.slice(eq + 1);
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
function resolveApiScopes(discovered, existing) {
|
|
140
|
+
const merged = mergeApiScopes(discovered, existing);
|
|
141
|
+
return merged.length > 0 ? merged : [
|
|
142
|
+
...auth0Cli.HELLO_API_SCOPES
|
|
143
|
+
].sort();
|
|
144
|
+
}
|
|
145
|
+
function upsertEnv(path, entries) {
|
|
146
|
+
let text = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
147
|
+
for (const [key, value] of Object.entries(entries)){
|
|
148
|
+
const re = new RegExp(`^${key}=.*$`, "m");
|
|
149
|
+
if (re.test(text)) {
|
|
150
|
+
text = text.replace(re, `${key}=${value}`);
|
|
151
|
+
} else {
|
|
152
|
+
text = `${text}${text === "" || text.endsWith("\n") ? "" : "\n"}${key}=${value}\n`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (text !== "" && !text.endsWith("\n")) text = `${text}\n`;
|
|
156
|
+
writeFileSync(path, text);
|
|
157
|
+
}
|
|
158
|
+
function patchPostmanAuth(targetDir, projectName, auth) {
|
|
159
|
+
const envYaml = join(targetDir, "postman/environments", `${projectName}-Local.environment.yaml`);
|
|
160
|
+
if (existsSync(envYaml)) {
|
|
161
|
+
let yaml = readFileSync(envYaml, "utf8");
|
|
162
|
+
yaml = replaceYamlKeyedValue(yaml, "auth0Domain", auth.domain);
|
|
163
|
+
yaml = replaceYamlKeyedValue(yaml, "auth0Audience", auth.audience);
|
|
164
|
+
yaml = replaceYamlKeyedValue(yaml, "auth0ClientId", auth.clientId);
|
|
165
|
+
yaml = replaceYamlKeyedValue(yaml, "authUrl", `https://${auth.domain}/authorize?audience=${auth.audience}`);
|
|
166
|
+
yaml = replaceYamlKeyedValue(yaml, "accessTokenUrl", `https://${auth.domain}/oauth/token`);
|
|
167
|
+
yaml = replaceYamlKeyedValue(yaml, "scope", auth.scopes);
|
|
168
|
+
writeFileSync(envYaml, yaml);
|
|
169
|
+
console.log(` ✓ Updated Postman environment auth0ClientId (${auth.clientId})`);
|
|
170
|
+
console.log(` ✓ Updated Postman token scopes`);
|
|
171
|
+
}
|
|
172
|
+
const collectionDef = join(targetDir, "postman/collections", `${projectName} WebAPI`, ".resources/definition.yaml");
|
|
173
|
+
if (existsSync(collectionDef)) {
|
|
174
|
+
let yaml = readFileSync(collectionDef, "utf8");
|
|
175
|
+
yaml = yaml.replace(/^(\s*clientId:\s*).+$/m, `$1${JSON.stringify(auth.clientId)}`);
|
|
176
|
+
yaml = yaml.replace(/^(\s*scope:\s*).+$/m, `$1${JSON.stringify(auth.scopes)}`);
|
|
177
|
+
writeFileSync(collectionDef, yaml);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Replace `key: <name>` / `value: …` pairs in Postman Local View environment YAML. */ function replaceYamlKeyedValue(yaml, key, value) {
|
|
181
|
+
const quoted = JSON.stringify(value);
|
|
182
|
+
const re = new RegExp(`(key:\\s*${key}\\s*\\n\\s*value:\\s*)[^\\n]+`, "m");
|
|
183
|
+
if (!re.test(yaml)) return yaml;
|
|
184
|
+
return yaml.replace(re, `$1${quoted}`);
|
|
185
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { listExistingApiNames } from "./api-name.js";
|
|
4
|
+
/** OIDC scopes requested on the Postman SPA token (not Auth0 API permissions). */ export const OIDC_SCOPES = [
|
|
5
|
+
"openid",
|
|
6
|
+
"profile",
|
|
7
|
+
"email"
|
|
8
|
+
];
|
|
9
|
+
const AUTHORIZE_RE = /@Authorize\(\s*(["'])([^"']+)\1(?:\s*,\s*(["'])([^"']+)\3)?\s*\)/g;
|
|
10
|
+
function isAppSource(fileName) {
|
|
11
|
+
return fileName.endsWith(".ts") && !fileName.endsWith(".spec.ts") && !fileName.endsWith(".test.ts") && !fileName.endsWith(".d.ts");
|
|
12
|
+
}
|
|
13
|
+
/** Collect action:Subject scopes from Authorize decorators in each API src tree. */ export function discoverAuthorizeScopes(rootDir) {
|
|
14
|
+
const scopes = new Set();
|
|
15
|
+
for (const apiName of listExistingApiNames(rootDir)){
|
|
16
|
+
collectFromDir(join(rootDir, "projects", apiName, "src"), scopes);
|
|
17
|
+
}
|
|
18
|
+
return [
|
|
19
|
+
...scopes
|
|
20
|
+
].sort();
|
|
21
|
+
}
|
|
22
|
+
function collectFromDir(dir, scopes) {
|
|
23
|
+
if (!existsSync(dir)) return;
|
|
24
|
+
for (const entry of readdirSync(dir, {
|
|
25
|
+
withFileTypes: true
|
|
26
|
+
})){
|
|
27
|
+
const path = join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
collectFromDir(path, scopes);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!entry.isFile() || !isAppSource(entry.name)) continue;
|
|
33
|
+
const text = readFileSync(path, "utf8");
|
|
34
|
+
for (const match of text.matchAll(AUTHORIZE_RE)){
|
|
35
|
+
const action = match[2];
|
|
36
|
+
if (action == null || action === "") continue;
|
|
37
|
+
const subject = match[4] ?? "all";
|
|
38
|
+
scopes.add(`${action}:${subject}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Additive union so Auth0 CLI scope replace does not drop permissions already on the API. */ export function mergeApiScopes(discovered, existing) {
|
|
43
|
+
return [
|
|
44
|
+
...new Set([
|
|
45
|
+
...existing,
|
|
46
|
+
...discovered
|
|
47
|
+
])
|
|
48
|
+
].sort();
|
|
49
|
+
}
|
|
50
|
+
export function postmanRequestedScopes(apiScopes) {
|
|
51
|
+
return [
|
|
52
|
+
...OIDC_SCOPES,
|
|
53
|
+
...apiScopes
|
|
54
|
+
].join(" ");
|
|
55
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { resolveTargetDir } from "../resolve-target-dir.js";
|
|
2
|
+
import { parseApiNames } from "./api-name.js";
|
|
3
|
+
import { scaffoldApi } from "./create-api.js";
|
|
4
|
+
export function collectName(value, previous) {
|
|
5
|
+
previous.push(value);
|
|
6
|
+
return previous;
|
|
7
|
+
}
|
|
8
|
+
export function addWebapiFeatureOptions(cmd) {
|
|
9
|
+
return cmd.option("--database <provider>", "Database provider: none | postgres/typeorm").option("--auth <provider>", "Auth provider: none | auth0").option("--postman <provider>", "Postman provider: none | local | api").option("-y, --yes", "Install dependencies without prompting").option("--install <mode>", "Dependency install mode: ask | yes | no", "ask");
|
|
10
|
+
}
|
|
11
|
+
export function toApiOptions(directory, opts, extra = {}) {
|
|
12
|
+
const installMode = String(opts.install ?? "ask").toLowerCase();
|
|
13
|
+
const names = Array.isArray(opts.name) ? opts.name : typeof opts.name === "string" ? [
|
|
14
|
+
opts.name
|
|
15
|
+
] : extra.names;
|
|
16
|
+
return {
|
|
17
|
+
d: resolveTargetDir(directory, typeof opts.d === "string" ? opts.d : undefined),
|
|
18
|
+
databaseProvider: opts.database,
|
|
19
|
+
authProvider: opts.auth,
|
|
20
|
+
postmanProvider: opts.postman,
|
|
21
|
+
installDependencies: opts.yes === true ? "yes" : installMode === "yes" || installMode === "no" ? installMode : undefined,
|
|
22
|
+
names,
|
|
23
|
+
...extra
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function runScaffoldApi(normalized) {
|
|
27
|
+
parseApiNames(normalized.names);
|
|
28
|
+
const { closePrompt } = await import("../domain/prompt.js");
|
|
29
|
+
try {
|
|
30
|
+
await scaffoldApi(normalized);
|
|
31
|
+
} finally{
|
|
32
|
+
closePrompt();
|
|
33
|
+
}
|
|
34
|
+
}
|