slskit-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/COMMAND.md +636 -0
- package/LICENSE +21 -0
- package/NOTICE.md +22 -0
- package/README.md +168 -0
- package/bin/slskit.js +3 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +25 -0
- package/dist/commands/base-command.d.ts +5 -0
- package/dist/commands/base-command.js +29 -0
- package/dist/commands/configure/action.d.ts +2 -0
- package/dist/commands/configure/action.js +120 -0
- package/dist/commands/configure/aws-cli.d.ts +16 -0
- package/dist/commands/configure/aws-cli.js +135 -0
- package/dist/commands/configure/index.d.ts +2 -0
- package/dist/commands/configure/index.js +20 -0
- package/dist/commands/configure/prompts.d.ts +8 -0
- package/dist/commands/configure/prompts.js +117 -0
- package/dist/commands/configure/types.d.ts +24 -0
- package/dist/commands/configure/types.js +6 -0
- package/dist/commands/deploy/action.d.ts +2 -0
- package/dist/commands/deploy/action.js +115 -0
- package/dist/commands/deploy/index.d.ts +2 -0
- package/dist/commands/deploy/index.js +23 -0
- package/dist/commands/deploy/sam-deploy.d.ts +10 -0
- package/dist/commands/deploy/sam-deploy.js +127 -0
- package/dist/commands/deploy/scope.d.ts +9 -0
- package/dist/commands/deploy/scope.js +34 -0
- package/dist/commands/deploy/types.d.ts +20 -0
- package/dist/commands/deploy/types.js +2 -0
- package/dist/commands/env/action.d.ts +11 -0
- package/dist/commands/env/action.js +274 -0
- package/dist/commands/env/dotenv.d.ts +6 -0
- package/dist/commands/env/dotenv.js +99 -0
- package/dist/commands/env/index.d.ts +2 -0
- package/dist/commands/env/index.js +76 -0
- package/dist/commands/env/keys.d.ts +8 -0
- package/dist/commands/env/keys.js +41 -0
- package/dist/commands/env/parameters.d.ts +7 -0
- package/dist/commands/env/parameters.js +63 -0
- package/dist/commands/env/templates.d.ts +3 -0
- package/dist/commands/env/templates.js +65 -0
- package/dist/commands/env/types.d.ts +21 -0
- package/dist/commands/env/types.js +2 -0
- package/dist/commands/function/action.d.ts +2 -0
- package/dist/commands/function/action.js +71 -0
- package/dist/commands/function/generator.d.ts +8 -0
- package/dist/commands/function/generator.js +58 -0
- package/dist/commands/function/index.d.ts +2 -0
- package/dist/commands/function/index.js +26 -0
- package/dist/commands/function/manifest.d.ts +5 -0
- package/dist/commands/function/manifest.js +57 -0
- package/dist/commands/function/prompts.d.ts +18 -0
- package/dist/commands/function/prompts.js +196 -0
- package/dist/commands/function/tooling.d.ts +2 -0
- package/dist/commands/function/tooling.js +43 -0
- package/dist/commands/function/types.d.ts +52 -0
- package/dist/commands/function/types.js +2 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/init/action.d.ts +2 -0
- package/dist/commands/init/action.js +36 -0
- package/dist/commands/init/content.d.ts +2 -0
- package/dist/commands/init/content.js +97 -0
- package/dist/commands/init/index.d.ts +2 -0
- package/dist/commands/init/index.js +30 -0
- package/dist/commands/init/manifest.d.ts +6 -0
- package/dist/commands/init/manifest.js +147 -0
- package/dist/commands/init/prompts.d.ts +6 -0
- package/dist/commands/init/prompts.js +155 -0
- package/dist/commands/init/scaffold.d.ts +7 -0
- package/dist/commands/init/scaffold.js +85 -0
- package/dist/commands/init/templates/helpers.d.ts +9 -0
- package/dist/commands/init/templates/helpers.js +42 -0
- package/dist/commands/init/templates/node.d.ts +4 -0
- package/dist/commands/init/templates/node.js +99 -0
- package/dist/commands/init/templates/project.d.ts +8 -0
- package/dist/commands/init/templates/project.js +239 -0
- package/dist/commands/init/templates/python.d.ts +4 -0
- package/dist/commands/init/templates/python.js +65 -0
- package/dist/commands/init/templates/sam.d.ts +4 -0
- package/dist/commands/init/templates/sam.js +369 -0
- package/dist/commands/init/templates/shared-code.d.ts +6 -0
- package/dist/commands/init/templates/shared-code.js +159 -0
- package/dist/commands/init/types.d.ts +47 -0
- package/dist/commands/init/types.js +75 -0
- package/dist/commands/rm/action.d.ts +2 -0
- package/dist/commands/rm/action.js +89 -0
- package/dist/commands/rm/index.d.ts +2 -0
- package/dist/commands/rm/index.js +25 -0
- package/dist/commands/rm/plan.d.ts +19 -0
- package/dist/commands/rm/plan.js +78 -0
- package/dist/commands/rm/prompts.d.ts +4 -0
- package/dist/commands/rm/prompts.js +107 -0
- package/dist/commands/rm/types.d.ts +7 -0
- package/dist/commands/rm/types.js +2 -0
- package/dist/commands/run/action.d.ts +2 -0
- package/dist/commands/run/action.js +171 -0
- package/dist/commands/run/index.d.ts +2 -0
- package/dist/commands/run/index.js +21 -0
- package/dist/commands/run/local-template.d.ts +7 -0
- package/dist/commands/run/local-template.js +51 -0
- package/dist/commands/run/manifest.d.ts +2 -0
- package/dist/commands/run/manifest.js +20 -0
- package/dist/commands/run/resources.d.ts +20 -0
- package/dist/commands/run/resources.js +93 -0
- package/dist/commands/run/sam-cli.d.ts +10 -0
- package/dist/commands/run/sam-cli.js +183 -0
- package/dist/commands/run/types.d.ts +18 -0
- package/dist/commands/run/types.js +2 -0
- package/dist/commands/run/watch.d.ts +11 -0
- package/dist/commands/run/watch.js +131 -0
- package/dist/core/context.d.ts +15 -0
- package/dist/core/context.js +41 -0
- package/dist/core/credentials.d.ts +10 -0
- package/dist/core/credentials.js +22 -0
- package/dist/core/environments.d.ts +46 -0
- package/dist/core/environments.js +161 -0
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/flags-docs.d.ts +2 -0
- package/dist/core/flags-docs.js +63 -0
- package/dist/core/is-ci.d.ts +4 -0
- package/dist/core/is-ci.js +16 -0
- package/dist/core/logger.d.ts +4 -0
- package/dist/core/logger.js +19 -0
- package/dist/core/run.d.ts +1 -0
- package/dist/core/run.js +27 -0
- package/dist/core/scope.d.ts +33 -0
- package/dist/core/scope.js +108 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +9 -0
- package/dist/program.d.ts +2 -0
- package/dist/program.js +26 -0
- package/package.json +110 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface EnvAddOptions {
|
|
2
|
+
profile?: string;
|
|
3
|
+
region?: string;
|
|
4
|
+
stackName?: string;
|
|
5
|
+
skipVerify?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface EnvRemoveOptions {
|
|
8
|
+
yes?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface EnvSetOptions {
|
|
11
|
+
env?: string;
|
|
12
|
+
secret?: boolean;
|
|
13
|
+
ssm?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface EnvUnsetOptions {
|
|
16
|
+
env?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface EnvVarsOptions {
|
|
19
|
+
env?: string;
|
|
20
|
+
showSecrets?: boolean;
|
|
21
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.functionAction = functionAction;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const errors_js_1 = require("../../core/errors.js");
|
|
11
|
+
const keys_js_1 = require("../env/keys.js");
|
|
12
|
+
const logger_js_1 = require("../../core/logger.js");
|
|
13
|
+
const types_js_1 = require("../init/types.js");
|
|
14
|
+
const generator_js_1 = require("./generator.js");
|
|
15
|
+
const manifest_js_1 = require("./manifest.js");
|
|
16
|
+
const prompts_js_1 = require("./prompts.js");
|
|
17
|
+
const tooling_js_1 = require("./tooling.js");
|
|
18
|
+
function runNpmInstall(root) {
|
|
19
|
+
logger_js_1.logger.info("\nInstalling npm dependencies...");
|
|
20
|
+
const result = (0, node_child_process_1.spawnSync)("npm", ["install"], {
|
|
21
|
+
cwd: root,
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
shell: process.platform === "win32",
|
|
24
|
+
});
|
|
25
|
+
if (result.error) {
|
|
26
|
+
throw new errors_js_1.CliError(`Failed to run npm install: ${result.error.message}`);
|
|
27
|
+
}
|
|
28
|
+
if (result.status !== 0) {
|
|
29
|
+
throw new errors_js_1.CliError(`npm install exited with code ${result.status ?? 1}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function functionAction(options) {
|
|
33
|
+
const root = process.cwd();
|
|
34
|
+
const manifest = (0, manifest_js_1.readProjectManifest)(root);
|
|
35
|
+
const answers = (0, manifest_js_1.toInitAnswers)(manifest);
|
|
36
|
+
const apps = (0, manifest_js_1.toServiceDefs)(manifest);
|
|
37
|
+
const picked = await (0, prompts_js_1.collectFunctionAnswers)(manifest, options);
|
|
38
|
+
const fn = {
|
|
39
|
+
name: picked.functionName,
|
|
40
|
+
httpPath: (0, prompts_js_1.functionHttpPath)(picked.appName, picked.functionName),
|
|
41
|
+
method: picked.method,
|
|
42
|
+
runtime: picked.runtime,
|
|
43
|
+
memorySize: picked.memorySize,
|
|
44
|
+
};
|
|
45
|
+
const sameFamily = (0, types_js_1.sameRuntimeFamily)(picked.runtime, answers.runtime);
|
|
46
|
+
const { files, appTemplatePath, rootTemplatePath } = (0, generator_js_1.generateFunction)(answers, apps, picked.appName, picked.isNewApp, fn, manifest.structure.files, (0, keys_js_1.projectEnvKeys)(root, manifest), { version: manifest.version, environments: manifest.environments });
|
|
47
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
48
|
+
const fullPath = node_path_1.default.join(root, relativePath);
|
|
49
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(fullPath), { recursive: true });
|
|
50
|
+
node_fs_1.default.writeFileSync(fullPath, contents);
|
|
51
|
+
}
|
|
52
|
+
if ((0, tooling_js_1.ensureToolingForRuntime)(root, answers, picked.runtime)) {
|
|
53
|
+
runNpmInstall(root);
|
|
54
|
+
}
|
|
55
|
+
logger_js_1.logger.info(`\nAdded "${picked.functionName}" to application "${picked.appName}"${picked.isNewApp ? " (new application)" : ""}.`);
|
|
56
|
+
logger_js_1.logger.info(` language: ${picked.runtime}`);
|
|
57
|
+
logger_js_1.logger.info(` memory: ${picked.memorySize} MB`);
|
|
58
|
+
if (manifest.apiGateway.enabled) {
|
|
59
|
+
logger_js_1.logger.info(` route: ${picked.method} ${fn.httpPath}`);
|
|
60
|
+
}
|
|
61
|
+
logger_js_1.logger.info(` handler: ${types_js_1.SRC_DIR}/functions/${picked.appName}/${picked.functionName}`);
|
|
62
|
+
logger_js_1.logger.info(` service: ${types_js_1.SRC_DIR}/services/${picked.appName}/${picked.functionName}`);
|
|
63
|
+
logger_js_1.logger.info(` template: ${appTemplatePath}`);
|
|
64
|
+
if (rootTemplatePath) {
|
|
65
|
+
logger_js_1.logger.info(` template: ${rootTemplatePath} (root stack updated)`);
|
|
66
|
+
}
|
|
67
|
+
if (!sameFamily) {
|
|
68
|
+
logger_js_1.logger.info(`\nNote: "${picked.functionName}" uses a different language family than the rest of the project, so it was generated standalone (no shared layer, no database wiring).`);
|
|
69
|
+
}
|
|
70
|
+
logger_js_1.logger.info('\nRun "slskit run" to try it locally.');
|
|
71
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { InitAnswers, ServiceDef, ServiceFunction } from "../init/types.js";
|
|
2
|
+
import type { PreservedState } from "../init/manifest.js";
|
|
3
|
+
export interface GeneratedFunction {
|
|
4
|
+
files: Record<string, string>;
|
|
5
|
+
appTemplatePath: string;
|
|
6
|
+
rootTemplatePath?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function generateFunction(answers: InitAnswers, apps: ServiceDef[], appName: string, isNewApp: boolean, fn: ServiceFunction, existingGeneratedFiles: string[], envKeys?: string[], preserved?: PreservedState): GeneratedFunction;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateFunction = generateFunction;
|
|
4
|
+
const types_js_1 = require("../init/types.js");
|
|
5
|
+
const manifest_js_1 = require("../init/manifest.js");
|
|
6
|
+
const node_js_1 = require("../init/templates/node.js");
|
|
7
|
+
const python_js_1 = require("../init/templates/python.js");
|
|
8
|
+
const sam_js_1 = require("../init/templates/sam.js");
|
|
9
|
+
function handlerSource(answers, appName, fn, fnRuntime) {
|
|
10
|
+
if (fnRuntime === "python") {
|
|
11
|
+
return (0, python_js_1.pythonHandler)(appName, fn);
|
|
12
|
+
}
|
|
13
|
+
return (0, node_js_1.nodeHandler)(answers, appName, fn, fnRuntime === "typescript");
|
|
14
|
+
}
|
|
15
|
+
function serviceSource(answers, fn, fnRuntime, sameFamily) {
|
|
16
|
+
if (fnRuntime === "python") {
|
|
17
|
+
return sameFamily ? (0, python_js_1.pythonService)(answers, fn) : (0, python_js_1.standalonePythonService)(fn);
|
|
18
|
+
}
|
|
19
|
+
return sameFamily
|
|
20
|
+
? (0, node_js_1.nodeService)(answers, fn, fnRuntime === "typescript")
|
|
21
|
+
: (0, node_js_1.standaloneNodeService)(fn, fnRuntime === "typescript");
|
|
22
|
+
}
|
|
23
|
+
// Attaches `fn` to `appName` (existing or new) and regenerates every file that must
|
|
24
|
+
// stay consistent with it: the handler, the service, that app's own infra template,
|
|
25
|
+
// the root template.yaml when a SAM app is brand new, and slskit.json.
|
|
26
|
+
function generateFunction(answers, apps, appName, isNewApp, fn, existingGeneratedFiles, envKeys = [], preserved = {}) {
|
|
27
|
+
const fnRuntime = fn.runtime ?? answers.runtime;
|
|
28
|
+
const sameFamily = (0, types_js_1.sameRuntimeFamily)(fnRuntime, answers.runtime);
|
|
29
|
+
const files = {};
|
|
30
|
+
if (fnRuntime === "python") {
|
|
31
|
+
files[`${types_js_1.SRC_DIR}/__init__.py`] = "";
|
|
32
|
+
files[`${types_js_1.SRC_DIR}/functions/__init__.py`] = "";
|
|
33
|
+
files[`${types_js_1.SRC_DIR}/services/__init__.py`] = "";
|
|
34
|
+
files[`${types_js_1.SRC_DIR}/functions/${appName}/__init__.py`] = "";
|
|
35
|
+
files[`${types_js_1.SRC_DIR}/services/${appName}/__init__.py`] = "";
|
|
36
|
+
files[`${types_js_1.SRC_DIR}/functions/${appName}/${fn.name}/__init__.py`] = "";
|
|
37
|
+
}
|
|
38
|
+
files[`${types_js_1.SRC_DIR}/functions/${appName}/${fn.name}/${(0, types_js_1.handlerFileName)(fnRuntime)}`] =
|
|
39
|
+
handlerSource(answers, appName, fn, fnRuntime);
|
|
40
|
+
files[`${types_js_1.SRC_DIR}/services/${appName}/${fn.name}.${(0, types_js_1.sourceExt)(fnRuntime)}`] = serviceSource(answers, fn, fnRuntime, sameFamily);
|
|
41
|
+
const targetApp = {
|
|
42
|
+
name: appName,
|
|
43
|
+
functions: [...(apps.find((app) => app.name === appName)?.functions ?? []), fn],
|
|
44
|
+
};
|
|
45
|
+
const updatedApps = isNewApp
|
|
46
|
+
? [...apps, targetApp]
|
|
47
|
+
: apps.map((app) => (app.name === appName ? targetApp : app));
|
|
48
|
+
const appTemplatePath = (0, types_js_1.serviceTemplatePath)(appName);
|
|
49
|
+
files[appTemplatePath] = (0, sam_js_1.samServiceTemplate)(answers, targetApp, envKeys);
|
|
50
|
+
let rootTemplatePath;
|
|
51
|
+
if (isNewApp) {
|
|
52
|
+
rootTemplatePath = "template.yaml";
|
|
53
|
+
files[rootTemplatePath] = (0, sam_js_1.samRootTemplate)(answers, updatedApps, envKeys);
|
|
54
|
+
}
|
|
55
|
+
const generatedFiles = Array.from(new Set([...existingGeneratedFiles, ...Object.keys(files)])).sort();
|
|
56
|
+
files["slskit.json"] = `${JSON.stringify((0, manifest_js_1.buildProjectManifest)(answers, updatedApps, generatedFiles, preserved), null, 2)}\n`;
|
|
57
|
+
return { files, appTemplatePath, rootTemplatePath };
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerFunctionCommand = registerFunctionCommand;
|
|
4
|
+
const run_js_1 = require("../../core/run.js");
|
|
5
|
+
function registerFunctionCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("function")
|
|
8
|
+
.description("Add a function to an existing or new application in this project")
|
|
9
|
+
.argument("[name]", "Function name")
|
|
10
|
+
.option("--app <name>", "Existing application to attach the function to")
|
|
11
|
+
.option("--new-app <name>", "Create a new application with this name")
|
|
12
|
+
.option("--method <method>", "GET | POST | PUT | DELETE | PATCH")
|
|
13
|
+
.option("--memory <mb>", "Function memory size: 128 | 256 | 512 | 1024 | 2048 | 3008 | 4096 | 10240")
|
|
14
|
+
.option("-r, --runtime <runtime>", "typescript | javascript | python")
|
|
15
|
+
.action((name, options) => (0, run_js_1.runCommand)(async () => {
|
|
16
|
+
const { functionAction } = await import("./action.js");
|
|
17
|
+
await functionAction({
|
|
18
|
+
name: name ?? options.name,
|
|
19
|
+
app: options.app,
|
|
20
|
+
newApp: options.newApp,
|
|
21
|
+
method: options.method,
|
|
22
|
+
memory: options.memory,
|
|
23
|
+
runtime: options.runtime,
|
|
24
|
+
});
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { InitAnswers, ServiceDef } from "../init/types.js";
|
|
2
|
+
import type { ProjectManifest } from "./types.js";
|
|
3
|
+
export declare function readProjectManifest(cwd: string, command?: string): ProjectManifest;
|
|
4
|
+
export declare function toInitAnswers(manifest: ProjectManifest): InitAnswers;
|
|
5
|
+
export declare function toServiceDefs(manifest: ProjectManifest): ServiceDef[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readProjectManifest = readProjectManifest;
|
|
7
|
+
exports.toInitAnswers = toInitAnswers;
|
|
8
|
+
exports.toServiceDefs = toServiceDefs;
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const errors_js_1 = require("../../core/errors.js");
|
|
11
|
+
const environments_js_1 = require("../../core/environments.js");
|
|
12
|
+
function readProjectManifest(cwd, command = "slskit function") {
|
|
13
|
+
const manifestPath = (0, environments_js_1.manifestPathFor)(cwd);
|
|
14
|
+
if (!manifestPath) {
|
|
15
|
+
throw new errors_js_1.CliError(`No slskit.json found in ${cwd}. Run "slskit init" first, or run "${command}" from your project root.`);
|
|
16
|
+
}
|
|
17
|
+
const manifest = JSON.parse(node_fs_1.default.readFileSync(manifestPath, "utf8"));
|
|
18
|
+
if (manifest.framework?.id !== "sam") {
|
|
19
|
+
throw new errors_js_1.CliError(`"${command}" supports AWS SAM projects only (this project uses "${manifest.framework?.id ?? "unknown"}").`);
|
|
20
|
+
}
|
|
21
|
+
return manifest;
|
|
22
|
+
}
|
|
23
|
+
// The manifest stores the AWS Lambda runtime string (e.g. "nodejs20.x"), which loses
|
|
24
|
+
// the TypeScript/JavaScript distinction; the generated handler's extension still has it.
|
|
25
|
+
function runtimeFromHandlerFile(handlerFile) {
|
|
26
|
+
if (handlerFile.endsWith(".ts")) {
|
|
27
|
+
return "typescript";
|
|
28
|
+
}
|
|
29
|
+
if (handlerFile.endsWith(".py")) {
|
|
30
|
+
return "python";
|
|
31
|
+
}
|
|
32
|
+
return "javascript";
|
|
33
|
+
}
|
|
34
|
+
function toInitAnswers(manifest) {
|
|
35
|
+
return {
|
|
36
|
+
name: manifest.name,
|
|
37
|
+
runtime: manifest.runtime.id,
|
|
38
|
+
database: manifest.database.id,
|
|
39
|
+
apiGateway: manifest.apiGateway.enabled,
|
|
40
|
+
sharedApi: manifest.apiGateway.enabled && manifest.apiGateway.perService === false,
|
|
41
|
+
layer: manifest.layer.enabled,
|
|
42
|
+
memorySize: manifest.functions.memorySize,
|
|
43
|
+
force: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function toServiceDefs(manifest) {
|
|
47
|
+
return manifest.applications.map((app) => ({
|
|
48
|
+
name: app.name,
|
|
49
|
+
functions: app.functions.map((fn) => ({
|
|
50
|
+
name: fn.name,
|
|
51
|
+
httpPath: fn.apiGateway.path ?? `/${app.name}/${fn.name}`,
|
|
52
|
+
method: fn.apiGateway.method ?? "GET",
|
|
53
|
+
runtime: runtimeFromHandlerFile(fn.handlerFile),
|
|
54
|
+
memorySize: fn.memorySize,
|
|
55
|
+
})),
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HttpMethod, MemorySize, RuntimeId } from "../init/types.js";
|
|
2
|
+
import type { FunctionOptions, ProjectManifest } from "./types.js";
|
|
3
|
+
export declare function functionHttpPath(appName: string, functionName: string): string;
|
|
4
|
+
export declare function functionNameProblem(manifest: ProjectManifest, appName: string, value: string): string | undefined;
|
|
5
|
+
export declare function applicationNameProblem(manifest: ProjectManifest, value: string): string | undefined;
|
|
6
|
+
export interface ApplicationTarget {
|
|
7
|
+
appName: string;
|
|
8
|
+
isNewApp: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface FunctionAnswers {
|
|
11
|
+
appName: string;
|
|
12
|
+
isNewApp: boolean;
|
|
13
|
+
functionName: string;
|
|
14
|
+
method: HttpMethod;
|
|
15
|
+
memorySize: MemorySize;
|
|
16
|
+
runtime: RuntimeId;
|
|
17
|
+
}
|
|
18
|
+
export declare function collectFunctionAnswers(manifest: ProjectManifest, options: FunctionOptions): Promise<FunctionAnswers>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.functionHttpPath = functionHttpPath;
|
|
4
|
+
exports.functionNameProblem = functionNameProblem;
|
|
5
|
+
exports.applicationNameProblem = applicationNameProblem;
|
|
6
|
+
exports.collectFunctionAnswers = collectFunctionAnswers;
|
|
7
|
+
const errors_js_1 = require("../../core/errors.js");
|
|
8
|
+
const logger_js_1 = require("../../core/logger.js");
|
|
9
|
+
const prompts_js_1 = require("../init/prompts.js");
|
|
10
|
+
const types_js_1 = require("../init/types.js");
|
|
11
|
+
const HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
|
12
|
+
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9]*$/;
|
|
13
|
+
function parseMethod(value) {
|
|
14
|
+
if (!value) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const upper = value.trim().toUpperCase();
|
|
18
|
+
if (!HTTP_METHODS.includes(upper)) {
|
|
19
|
+
throw new errors_js_1.CliError(`Unknown HTTP method "${value}". Expected ${HTTP_METHODS.join(", ")}.`);
|
|
20
|
+
}
|
|
21
|
+
return upper;
|
|
22
|
+
}
|
|
23
|
+
// The route "slskit function" gives a new function. Kept here so the uniqueness
|
|
24
|
+
// check and the generator can never disagree about what is being created.
|
|
25
|
+
function functionHttpPath(appName, functionName) {
|
|
26
|
+
return `/${appName}/${functionName}`;
|
|
27
|
+
}
|
|
28
|
+
// A function name has to be unique across the whole project, not just its own
|
|
29
|
+
// application: the Lambda is physically named <project>-<environment>-<function>,
|
|
30
|
+
// and with one shared API Gateway the template's logical id is <Name>Function. Both
|
|
31
|
+
// are project-scoped, so reusing a name in another application silently replaces the
|
|
32
|
+
// first function instead of failing -- SAM validates and builds it without complaint.
|
|
33
|
+
function findFunctionNamed(manifest, name) {
|
|
34
|
+
const wanted = name.toLowerCase();
|
|
35
|
+
for (const app of manifest.applications) {
|
|
36
|
+
for (const fn of app.functions) {
|
|
37
|
+
if (fn.name.toLowerCase() === wanted) {
|
|
38
|
+
return { app: app.name, fn: fn.name };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function findRoute(manifest, method, path) {
|
|
45
|
+
for (const app of manifest.applications) {
|
|
46
|
+
for (const fn of app.functions) {
|
|
47
|
+
if (fn.apiGateway.enabled &&
|
|
48
|
+
fn.apiGateway.path === path &&
|
|
49
|
+
(fn.apiGateway.method ?? "GET").toUpperCase() === method.toUpperCase()) {
|
|
50
|
+
return { app: app.name, fn: fn.name };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
// Returns the reason this name cannot be used, or undefined when it is free.
|
|
57
|
+
function functionNameProblem(manifest, appName, value) {
|
|
58
|
+
const trimmed = value.trim();
|
|
59
|
+
if (!IDENTIFIER.test(trimmed)) {
|
|
60
|
+
return `Function name must start with a letter and contain only letters and numbers (got "${value}").`;
|
|
61
|
+
}
|
|
62
|
+
const clash = findFunctionNamed(manifest, trimmed);
|
|
63
|
+
if (clash) {
|
|
64
|
+
return clash.app === appName
|
|
65
|
+
? `Function "${clash.fn}" already exists in application "${appName}". Choose another name.`
|
|
66
|
+
: `Function "${clash.fn}" already exists in application "${clash.app}", and every function in a project needs its own name. Choose another name.`;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function applicationNameProblem(manifest, value) {
|
|
71
|
+
const trimmed = value.trim();
|
|
72
|
+
if (!IDENTIFIER.test(trimmed)) {
|
|
73
|
+
return `Application name must start with a letter and contain only letters and numbers (got "${value}").`;
|
|
74
|
+
}
|
|
75
|
+
if (manifest.applications.some((app) => app.name === trimmed)) {
|
|
76
|
+
return `Application "${trimmed}" already exists. Choose another name, or add to it with --app ${trimmed}.`;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
async function resolveApplication(manifest, options) {
|
|
81
|
+
if (options.app && options.newApp) {
|
|
82
|
+
throw new errors_js_1.CliError('Pass either "--app" or "--new-app", not both.');
|
|
83
|
+
}
|
|
84
|
+
if (options.newApp) {
|
|
85
|
+
const problem = applicationNameProblem(manifest, options.newApp);
|
|
86
|
+
if (problem) {
|
|
87
|
+
throw new errors_js_1.CliError(problem);
|
|
88
|
+
}
|
|
89
|
+
return { appName: options.newApp.trim(), isNewApp: true };
|
|
90
|
+
}
|
|
91
|
+
if (options.app) {
|
|
92
|
+
const existing = manifest.applications.find((app) => app.name === options.app);
|
|
93
|
+
if (!existing) {
|
|
94
|
+
const known = manifest.applications.map((app) => app.name).join(", ") || "(none)";
|
|
95
|
+
throw new errors_js_1.CliError(`Application "${options.app}" was not found. Existing: ${known}.`);
|
|
96
|
+
}
|
|
97
|
+
return { appName: existing.name, isNewApp: false };
|
|
98
|
+
}
|
|
99
|
+
if (!process.stdin.isTTY) {
|
|
100
|
+
throw new errors_js_1.CliError('Non-interactive mode needs "--app <name>" or "--new-app <name>".');
|
|
101
|
+
}
|
|
102
|
+
const { select, input } = await import("@inquirer/prompts");
|
|
103
|
+
const target = await select({
|
|
104
|
+
message: "Add this function to:",
|
|
105
|
+
choices: [
|
|
106
|
+
{ name: "An existing application", value: "existing" },
|
|
107
|
+
{ name: "A new application", value: "new" },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
if (target === "new") {
|
|
111
|
+
// validate re-asks in place, so a name that is taken costs one keystroke
|
|
112
|
+
// rather than the whole command.
|
|
113
|
+
const name = await input({
|
|
114
|
+
message: "New application name:",
|
|
115
|
+
required: true,
|
|
116
|
+
validate: (value) => applicationNameProblem(manifest, value) ?? true,
|
|
117
|
+
});
|
|
118
|
+
return { appName: name.trim(), isNewApp: true };
|
|
119
|
+
}
|
|
120
|
+
if (manifest.applications.length === 0) {
|
|
121
|
+
throw new errors_js_1.CliError('This project has no applications yet. Choose "A new application" instead.');
|
|
122
|
+
}
|
|
123
|
+
const appName = await select({
|
|
124
|
+
message: "Application:",
|
|
125
|
+
choices: manifest.applications.map((app) => ({
|
|
126
|
+
name: `${app.name} (${app.functions.length} function${app.functions.length === 1 ? "" : "s"})`,
|
|
127
|
+
value: app.name,
|
|
128
|
+
})),
|
|
129
|
+
});
|
|
130
|
+
const existingApp = manifest.applications.find((app) => app.name === appName);
|
|
131
|
+
if (existingApp && existingApp.functions.length > 0) {
|
|
132
|
+
logger_js_1.logger.info(`\nExisting functions in "${appName}": ${existingApp.functions.map((fn) => fn.name).join(", ")}`);
|
|
133
|
+
}
|
|
134
|
+
return { appName, isNewApp: false };
|
|
135
|
+
}
|
|
136
|
+
async function collectFunctionAnswers(manifest, options) {
|
|
137
|
+
const { appName, isNewApp } = await resolveApplication(manifest, options);
|
|
138
|
+
let functionName = options.name?.trim();
|
|
139
|
+
let method = parseMethod(options.method);
|
|
140
|
+
let memorySize = (0, prompts_js_1.parseMemory)(options.memory);
|
|
141
|
+
let runtime = (0, prompts_js_1.parseAlias)(options.runtime, prompts_js_1.RUNTIME_ALIASES, "runtime");
|
|
142
|
+
const needsPrompt = !functionName || (manifest.apiGateway.enabled && !method) || !memorySize || !runtime;
|
|
143
|
+
if (needsPrompt && !process.stdin.isTTY) {
|
|
144
|
+
throw new errors_js_1.CliError('Non-interactive mode needs a function name plus "--method", "--memory", and "--runtime".');
|
|
145
|
+
}
|
|
146
|
+
if (needsPrompt) {
|
|
147
|
+
const { input, select } = await import("@inquirer/prompts");
|
|
148
|
+
functionName ?? (functionName = await input({
|
|
149
|
+
message: "Function name:",
|
|
150
|
+
required: true,
|
|
151
|
+
validate: (value) => functionNameProblem(manifest, appName, value) ?? true,
|
|
152
|
+
}));
|
|
153
|
+
if (manifest.apiGateway.enabled) {
|
|
154
|
+
method ?? (method = await select({
|
|
155
|
+
message: "HTTP method:",
|
|
156
|
+
choices: HTTP_METHODS.map((value) => ({ name: value, value })),
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
memorySize ?? (memorySize = await select({
|
|
160
|
+
message: "Function memory size:",
|
|
161
|
+
default: manifest.functions.memorySize,
|
|
162
|
+
choices: types_js_1.MEMORY_SIZES.map((size) => ({ name: (0, prompts_js_1.memoryLabel)(size), value: size })),
|
|
163
|
+
}));
|
|
164
|
+
runtime ?? (runtime = await select({
|
|
165
|
+
message: "Language:",
|
|
166
|
+
default: manifest.runtime.id,
|
|
167
|
+
choices: [
|
|
168
|
+
{ name: "TypeScript (Node.js)", value: "typescript" },
|
|
169
|
+
{ name: "JavaScript (Node.js)", value: "javascript" },
|
|
170
|
+
{ name: "Python", value: "python" },
|
|
171
|
+
],
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
const name = (functionName ?? "").trim();
|
|
175
|
+
// Reached with --name on a non-interactive run, where there is nothing to re-ask.
|
|
176
|
+
const problem = functionNameProblem(manifest, appName, name);
|
|
177
|
+
if (problem) {
|
|
178
|
+
throw new errors_js_1.CliError(problem);
|
|
179
|
+
}
|
|
180
|
+
const path = functionHttpPath(appName, name);
|
|
181
|
+
const chosenMethod = method ?? "GET";
|
|
182
|
+
const routeClash = manifest.apiGateway.enabled
|
|
183
|
+
? findRoute(manifest, chosenMethod, path)
|
|
184
|
+
: undefined;
|
|
185
|
+
if (routeClash) {
|
|
186
|
+
throw new errors_js_1.CliError(`${chosenMethod} ${path} is already served by "${routeClash.app}/${routeClash.fn}". Choose another name or method.`);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
appName,
|
|
190
|
+
isNewApp,
|
|
191
|
+
functionName: name,
|
|
192
|
+
method: chosenMethod,
|
|
193
|
+
memorySize: memorySize,
|
|
194
|
+
runtime: runtime,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ensureToolingForRuntime = ensureToolingForRuntime;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const project_js_1 = require("../init/templates/project.js");
|
|
10
|
+
// A function whose language differs from the project's original runtime may need
|
|
11
|
+
// tooling the project never installed (esbuild for a first node function in a
|
|
12
|
+
// python project, or typescript for a first TS function in a plain JS project).
|
|
13
|
+
// Returns true if package.json changed, so the caller knows to re-run npm install.
|
|
14
|
+
function ensureToolingForRuntime(root, answers, fnRuntime) {
|
|
15
|
+
if (fnRuntime === "python") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const pkgPath = node_path_1.default.join(root, "package.json");
|
|
19
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
|
|
20
|
+
pkg.dependencies ?? (pkg.dependencies = {});
|
|
21
|
+
pkg.devDependencies ?? (pkg.devDependencies = {});
|
|
22
|
+
let changed = false;
|
|
23
|
+
if (!pkg.dependencies.esbuild && !pkg.devDependencies.esbuild) {
|
|
24
|
+
// sam build stages the project with `npm install --omit=dev`, so esbuild must be a regular dependency.
|
|
25
|
+
pkg.dependencies.esbuild = "^0.25.0";
|
|
26
|
+
changed = true;
|
|
27
|
+
}
|
|
28
|
+
if (fnRuntime === "typescript" && !pkg.devDependencies.typescript) {
|
|
29
|
+
pkg.devDependencies.typescript = "^5.7.2";
|
|
30
|
+
pkg.devDependencies["@types/node"] = "^22.10.2";
|
|
31
|
+
pkg.devDependencies["@types/aws-lambda"] = "^8.10.145";
|
|
32
|
+
changed = true;
|
|
33
|
+
}
|
|
34
|
+
if (changed) {
|
|
35
|
+
node_fs_1.default.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
36
|
+
}
|
|
37
|
+
const tsconfigPath = node_path_1.default.join(root, "tsconfig.json");
|
|
38
|
+
if (fnRuntime === "typescript" && !node_fs_1.default.existsSync(tsconfigPath)) {
|
|
39
|
+
node_fs_1.default.writeFileSync(tsconfigPath, (0, project_js_1.tsconfig)(answers));
|
|
40
|
+
changed = true;
|
|
41
|
+
}
|
|
42
|
+
return changed;
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EnvironmentsConfig } from "../../core/environments.js";
|
|
2
|
+
import type { DatabaseId, MemorySize, RuntimeId } from "../init/types.js";
|
|
3
|
+
export interface FunctionOptions {
|
|
4
|
+
name?: string;
|
|
5
|
+
app?: string;
|
|
6
|
+
newApp?: string;
|
|
7
|
+
method?: string;
|
|
8
|
+
memory?: string | number;
|
|
9
|
+
runtime?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ManifestFunctionEntry {
|
|
12
|
+
name: string;
|
|
13
|
+
handlerFile: string;
|
|
14
|
+
memorySize: MemorySize;
|
|
15
|
+
apiGateway: {
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
path?: string;
|
|
18
|
+
method?: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface ManifestApplication {
|
|
22
|
+
name: string;
|
|
23
|
+
functions: ManifestFunctionEntry[];
|
|
24
|
+
}
|
|
25
|
+
export interface ProjectManifest {
|
|
26
|
+
name: string;
|
|
27
|
+
runtime: {
|
|
28
|
+
id: RuntimeId;
|
|
29
|
+
};
|
|
30
|
+
framework: {
|
|
31
|
+
id: string;
|
|
32
|
+
};
|
|
33
|
+
database: {
|
|
34
|
+
id: DatabaseId;
|
|
35
|
+
};
|
|
36
|
+
apiGateway: {
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
perService?: boolean;
|
|
39
|
+
};
|
|
40
|
+
layer: {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
};
|
|
43
|
+
functions: {
|
|
44
|
+
memorySize: MemorySize;
|
|
45
|
+
};
|
|
46
|
+
applications: ManifestApplication[];
|
|
47
|
+
structure: {
|
|
48
|
+
files: string[];
|
|
49
|
+
};
|
|
50
|
+
environments?: EnvironmentsConfig;
|
|
51
|
+
version?: string;
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCommands = registerCommands;
|
|
4
|
+
const index_js_1 = require("./configure/index.js");
|
|
5
|
+
const index_js_2 = require("./deploy/index.js");
|
|
6
|
+
const index_js_3 = require("./env/index.js");
|
|
7
|
+
const index_js_4 = require("./function/index.js");
|
|
8
|
+
const index_js_5 = require("./init/index.js");
|
|
9
|
+
const index_js_6 = require("./rm/index.js");
|
|
10
|
+
const index_js_7 = require("./run/index.js");
|
|
11
|
+
function registerCommands(program) {
|
|
12
|
+
(0, index_js_5.registerInitCommand)(program);
|
|
13
|
+
(0, index_js_7.registerRunCommand)(program);
|
|
14
|
+
(0, index_js_4.registerFunctionCommand)(program);
|
|
15
|
+
(0, index_js_6.registerRmCommand)(program);
|
|
16
|
+
(0, index_js_1.registerConfigureCommand)(program);
|
|
17
|
+
(0, index_js_3.registerEnvCommand)(program);
|
|
18
|
+
(0, index_js_2.registerDeployCommand)(program);
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initAction = initAction;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const logger_js_1 = require("../../core/logger.js");
|
|
9
|
+
const prompts_js_1 = require("./prompts.js");
|
|
10
|
+
const scaffold_js_1 = require("./scaffold.js");
|
|
11
|
+
const types_js_1 = require("./types.js");
|
|
12
|
+
async function initAction(options) {
|
|
13
|
+
const answers = await (0, prompts_js_1.collectAnswers)(options);
|
|
14
|
+
const root = await (0, scaffold_js_1.scaffoldProject)(answers);
|
|
15
|
+
const relative = node_path_1.default.relative(process.cwd(), root) || ".";
|
|
16
|
+
logger_js_1.logger.info(`\nInitialized slskit project "${answers.name}" in ${relative}`);
|
|
17
|
+
for (const app of types_js_1.LAMBDA_APPS) {
|
|
18
|
+
logger_js_1.logger.info(` ${types_js_1.SRC_DIR}/functions/${app.name} (handlers)`);
|
|
19
|
+
}
|
|
20
|
+
for (const app of types_js_1.LAMBDA_APPS) {
|
|
21
|
+
logger_js_1.logger.info(` ${types_js_1.SRC_DIR}/services/${app.name}`);
|
|
22
|
+
}
|
|
23
|
+
logger_js_1.logger.info(answers.layer
|
|
24
|
+
? ` ${types_js_1.SRC_DIR}/shared (common Lambda layer)`
|
|
25
|
+
: ` ${types_js_1.SRC_DIR}/shared`);
|
|
26
|
+
// Every project is a root stack nesting one template per service; the API mode only
|
|
27
|
+
// decides whether that API lives in the root or in each service.
|
|
28
|
+
for (const app of types_js_1.LAMBDA_APPS) {
|
|
29
|
+
logger_js_1.logger.info(` ${(0, types_js_1.serviceTemplatePath)(app.name)} (${app.name} service stack)`);
|
|
30
|
+
}
|
|
31
|
+
logger_js_1.logger.info(answers.sharedApi
|
|
32
|
+
? ` ${types_js_1.INFRA_FILE} (root stack, owns the one API Gateway)`
|
|
33
|
+
: ` ${types_js_1.INFRA_FILE} (root stack)`);
|
|
34
|
+
logger_js_1.logger.info(` memory: ${answers.memorySize} MB`);
|
|
35
|
+
logger_js_1.logger.info(" slskit.json");
|
|
36
|
+
}
|