@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,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,46 @@
|
|
|
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
|
+
export async function runCli(argv = process.argv) {
|
|
32
|
+
try {
|
|
33
|
+
await createProgram().parseAsync(argv);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err instanceof CommanderError) {
|
|
36
|
+
process.exit(err.exitCode);
|
|
37
|
+
}
|
|
38
|
+
if (err instanceof CreateCliError) {
|
|
39
|
+
console.error(`\n ${err.message}\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
console.error(err);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
process.exit(process.exitCode ?? 0);
|
|
46
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Default API project slug when `--name` is omitted. */ export const DEFAULT_WEBAPI_NAME = "api";
|
|
2
|
+
/** Relative path of an API project inside the monorepo. */ export function webapiProjectDir(name) {
|
|
3
|
+
return `projects/${name}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Forge release entry for a scaffolded API (`pack: true` → per-target outDir).
|
|
7
|
+
* Keep in sync with root `package.json` `"start"` / `"start:<name>"` and Dockerfiles.
|
|
8
|
+
*/ export function webapiReleaseEntry(name) {
|
|
9
|
+
return `dist/release/${webapiProjectDir(name)}/${webapiPackagedEntry()}`;
|
|
10
|
+
}
|
|
11
|
+
/** The same entry inside `.package/` (`forge package`), which is what the Dockerfile runs. */ export function webapiPackagedEntry() {
|
|
12
|
+
return "es6/src/main.js";
|
|
13
|
+
}
|
|
14
|
+
/** npm package name for a scaffolded API (`flux serve --project`). */ export function webapiPackageName(workspaceName, apiName) {
|
|
15
|
+
return `@${workspaceName}/${apiName}`;
|
|
16
|
+
}
|
|
17
|
+
/** Local listen port for the nth API (0-based). Production uses `PORT`. */ export function webapiPort(index) {
|
|
18
|
+
return 3000 + index;
|
|
19
|
+
}
|
|
20
|
+
/** @deprecated Use `webapiProjectDir(name)`. Default path for the first API. */ export const WEBAPI_PROJECT_DIR = webapiProjectDir(DEFAULT_WEBAPI_NAME);
|
|
21
|
+
/** @deprecated Use `webapiReleaseEntry(name)`. Default start entry for the first API. */ export const WEBAPI_RELEASE_ENTRY = webapiReleaseEntry(DEFAULT_WEBAPI_NAME);
|