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.
Files changed (135) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/COMMAND.md +636 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +22 -0
  5. package/README.md +168 -0
  6. package/bin/slskit.js +3 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +25 -0
  9. package/dist/commands/base-command.d.ts +5 -0
  10. package/dist/commands/base-command.js +29 -0
  11. package/dist/commands/configure/action.d.ts +2 -0
  12. package/dist/commands/configure/action.js +120 -0
  13. package/dist/commands/configure/aws-cli.d.ts +16 -0
  14. package/dist/commands/configure/aws-cli.js +135 -0
  15. package/dist/commands/configure/index.d.ts +2 -0
  16. package/dist/commands/configure/index.js +20 -0
  17. package/dist/commands/configure/prompts.d.ts +8 -0
  18. package/dist/commands/configure/prompts.js +117 -0
  19. package/dist/commands/configure/types.d.ts +24 -0
  20. package/dist/commands/configure/types.js +6 -0
  21. package/dist/commands/deploy/action.d.ts +2 -0
  22. package/dist/commands/deploy/action.js +115 -0
  23. package/dist/commands/deploy/index.d.ts +2 -0
  24. package/dist/commands/deploy/index.js +23 -0
  25. package/dist/commands/deploy/sam-deploy.d.ts +10 -0
  26. package/dist/commands/deploy/sam-deploy.js +127 -0
  27. package/dist/commands/deploy/scope.d.ts +9 -0
  28. package/dist/commands/deploy/scope.js +34 -0
  29. package/dist/commands/deploy/types.d.ts +20 -0
  30. package/dist/commands/deploy/types.js +2 -0
  31. package/dist/commands/env/action.d.ts +11 -0
  32. package/dist/commands/env/action.js +274 -0
  33. package/dist/commands/env/dotenv.d.ts +6 -0
  34. package/dist/commands/env/dotenv.js +99 -0
  35. package/dist/commands/env/index.d.ts +2 -0
  36. package/dist/commands/env/index.js +76 -0
  37. package/dist/commands/env/keys.d.ts +8 -0
  38. package/dist/commands/env/keys.js +41 -0
  39. package/dist/commands/env/parameters.d.ts +7 -0
  40. package/dist/commands/env/parameters.js +63 -0
  41. package/dist/commands/env/templates.d.ts +3 -0
  42. package/dist/commands/env/templates.js +65 -0
  43. package/dist/commands/env/types.d.ts +21 -0
  44. package/dist/commands/env/types.js +2 -0
  45. package/dist/commands/function/action.d.ts +2 -0
  46. package/dist/commands/function/action.js +71 -0
  47. package/dist/commands/function/generator.d.ts +8 -0
  48. package/dist/commands/function/generator.js +58 -0
  49. package/dist/commands/function/index.d.ts +2 -0
  50. package/dist/commands/function/index.js +26 -0
  51. package/dist/commands/function/manifest.d.ts +5 -0
  52. package/dist/commands/function/manifest.js +57 -0
  53. package/dist/commands/function/prompts.d.ts +18 -0
  54. package/dist/commands/function/prompts.js +196 -0
  55. package/dist/commands/function/tooling.d.ts +2 -0
  56. package/dist/commands/function/tooling.js +43 -0
  57. package/dist/commands/function/types.d.ts +52 -0
  58. package/dist/commands/function/types.js +2 -0
  59. package/dist/commands/index.d.ts +2 -0
  60. package/dist/commands/index.js +19 -0
  61. package/dist/commands/init/action.d.ts +2 -0
  62. package/dist/commands/init/action.js +36 -0
  63. package/dist/commands/init/content.d.ts +2 -0
  64. package/dist/commands/init/content.js +97 -0
  65. package/dist/commands/init/index.d.ts +2 -0
  66. package/dist/commands/init/index.js +30 -0
  67. package/dist/commands/init/manifest.d.ts +6 -0
  68. package/dist/commands/init/manifest.js +147 -0
  69. package/dist/commands/init/prompts.d.ts +6 -0
  70. package/dist/commands/init/prompts.js +155 -0
  71. package/dist/commands/init/scaffold.d.ts +7 -0
  72. package/dist/commands/init/scaffold.js +85 -0
  73. package/dist/commands/init/templates/helpers.d.ts +9 -0
  74. package/dist/commands/init/templates/helpers.js +42 -0
  75. package/dist/commands/init/templates/node.d.ts +4 -0
  76. package/dist/commands/init/templates/node.js +99 -0
  77. package/dist/commands/init/templates/project.d.ts +8 -0
  78. package/dist/commands/init/templates/project.js +239 -0
  79. package/dist/commands/init/templates/python.d.ts +4 -0
  80. package/dist/commands/init/templates/python.js +65 -0
  81. package/dist/commands/init/templates/sam.d.ts +4 -0
  82. package/dist/commands/init/templates/sam.js +369 -0
  83. package/dist/commands/init/templates/shared-code.d.ts +6 -0
  84. package/dist/commands/init/templates/shared-code.js +159 -0
  85. package/dist/commands/init/types.d.ts +47 -0
  86. package/dist/commands/init/types.js +75 -0
  87. package/dist/commands/rm/action.d.ts +2 -0
  88. package/dist/commands/rm/action.js +89 -0
  89. package/dist/commands/rm/index.d.ts +2 -0
  90. package/dist/commands/rm/index.js +25 -0
  91. package/dist/commands/rm/plan.d.ts +19 -0
  92. package/dist/commands/rm/plan.js +78 -0
  93. package/dist/commands/rm/prompts.d.ts +4 -0
  94. package/dist/commands/rm/prompts.js +107 -0
  95. package/dist/commands/rm/types.d.ts +7 -0
  96. package/dist/commands/rm/types.js +2 -0
  97. package/dist/commands/run/action.d.ts +2 -0
  98. package/dist/commands/run/action.js +171 -0
  99. package/dist/commands/run/index.d.ts +2 -0
  100. package/dist/commands/run/index.js +21 -0
  101. package/dist/commands/run/local-template.d.ts +7 -0
  102. package/dist/commands/run/local-template.js +51 -0
  103. package/dist/commands/run/manifest.d.ts +2 -0
  104. package/dist/commands/run/manifest.js +20 -0
  105. package/dist/commands/run/resources.d.ts +20 -0
  106. package/dist/commands/run/resources.js +93 -0
  107. package/dist/commands/run/sam-cli.d.ts +10 -0
  108. package/dist/commands/run/sam-cli.js +183 -0
  109. package/dist/commands/run/types.d.ts +18 -0
  110. package/dist/commands/run/types.js +2 -0
  111. package/dist/commands/run/watch.d.ts +11 -0
  112. package/dist/commands/run/watch.js +131 -0
  113. package/dist/core/context.d.ts +15 -0
  114. package/dist/core/context.js +41 -0
  115. package/dist/core/credentials.d.ts +10 -0
  116. package/dist/core/credentials.js +22 -0
  117. package/dist/core/environments.d.ts +46 -0
  118. package/dist/core/environments.js +161 -0
  119. package/dist/core/errors.d.ts +4 -0
  120. package/dist/core/errors.js +11 -0
  121. package/dist/core/flags-docs.d.ts +2 -0
  122. package/dist/core/flags-docs.js +63 -0
  123. package/dist/core/is-ci.d.ts +4 -0
  124. package/dist/core/is-ci.js +16 -0
  125. package/dist/core/logger.d.ts +4 -0
  126. package/dist/core/logger.js +19 -0
  127. package/dist/core/run.d.ts +1 -0
  128. package/dist/core/run.js +27 -0
  129. package/dist/core/scope.d.ts +33 -0
  130. package/dist/core/scope.js +108 -0
  131. package/dist/index.d.ts +3 -0
  132. package/dist/index.js +9 -0
  133. package/dist/program.d.ts +2 -0
  134. package/dist/program.js +26 -0
  135. package/package.json +110 -0
@@ -0,0 +1,274 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stackNameFor = void 0;
4
+ exports.envListAction = envListAction;
5
+ exports.envAddAction = envAddAction;
6
+ exports.envUseAction = envUseAction;
7
+ exports.envRemoveAction = envRemoveAction;
8
+ exports.envSetAction = envSetAction;
9
+ exports.envUnsetAction = envUnsetAction;
10
+ exports.envVarsAction = envVarsAction;
11
+ exports.seedDotenv = seedDotenv;
12
+ const errors_js_1 = require("../../core/errors.js");
13
+ const is_ci_js_1 = require("../../core/is-ci.js");
14
+ const logger_js_1 = require("../../core/logger.js");
15
+ const environments_js_1 = require("../../core/environments.js");
16
+ Object.defineProperty(exports, "stackNameFor", { enumerable: true, get: function () { return environments_js_1.stackNameFor; } });
17
+ const action_js_1 = require("../configure/action.js");
18
+ const keys_js_1 = require("./keys.js");
19
+ const dotenv_js_1 = require("./dotenv.js");
20
+ const templates_js_1 = require("./templates.js");
21
+ const KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
22
+ function requireName(name) {
23
+ const parsed = (0, environments_js_1.parseEnvironmentName)(name);
24
+ if (!parsed) {
25
+ throw new errors_js_1.CliError("An environment name is required.");
26
+ }
27
+ return parsed;
28
+ }
29
+ function parseKey(key) {
30
+ const trimmed = key.trim();
31
+ if (!KEY_PATTERN.test(trimmed)) {
32
+ throw new errors_js_1.CliError(`Invalid variable name "${key}". Use letters, digits and underscores, starting with a letter or underscore.`);
33
+ }
34
+ if (trimmed === environments_js_1.APP_ENVIRONMENT_KEY) {
35
+ throw new errors_js_1.CliError(`${environments_js_1.APP_ENVIRONMENT_KEY} is managed by slskit and always equals the environment name. Use "slskit env add <name>" to create another environment instead.`);
36
+ }
37
+ return trimmed;
38
+ }
39
+ // Every function's physical name is <project>-<environment>-<function>, so a long
40
+ // environment name can push it past Lambda's limit. Caught before anything is written.
41
+ function assertNamesFit(manifest, environment) {
42
+ const applications = manifest.applications ?? [];
43
+ for (const app of applications) {
44
+ for (const fn of app.functions ?? []) {
45
+ (0, environments_js_1.assertFunctionNameFits)(manifest.name, environment, fn.name);
46
+ }
47
+ }
48
+ }
49
+ async function envListAction() {
50
+ const cwd = process.cwd();
51
+ const manifest = (0, environments_js_1.readManifest)(cwd, "slskit env list");
52
+ const names = (0, environments_js_1.environmentNames)(manifest);
53
+ if (names.length === 0) {
54
+ logger_js_1.logger.info('\nThis project has no environments yet. Add one with "slskit env add dev".');
55
+ return;
56
+ }
57
+ const active = manifest.environments?.default;
58
+ logger_js_1.logger.info(`\nEnvironments for "${manifest.name}":\n`);
59
+ for (const name of names) {
60
+ const config = (0, environments_js_1.requireEnvironment)(manifest, name);
61
+ const marker = name === active ? "*" : " ";
62
+ const count = Object.keys(config.variables ?? {}).length;
63
+ logger_js_1.logger.info(`${marker} ${name}`);
64
+ logger_js_1.logger.info(` ${environments_js_1.APP_ENVIRONMENT_KEY}: ${name}`);
65
+ logger_js_1.logger.info(` stack: ${config.stackName}`);
66
+ logger_js_1.logger.info(` region: ${config.region ?? "(not configured)"}`);
67
+ logger_js_1.logger.info(` profile: ${config.profile ?? "(environment)"}`);
68
+ logger_js_1.logger.info(` variables: ${count}`);
69
+ }
70
+ logger_js_1.logger.info("\n* default environment");
71
+ }
72
+ // An environment is a deploy target plus a name, so creating one runs the same
73
+ // credential flow as configure rather than duplicating it.
74
+ async function envAddAction(name, options) {
75
+ const cwd = process.cwd();
76
+ const manifest = (0, environments_js_1.readManifest)(cwd, "slskit env add");
77
+ const environment = requireName(name);
78
+ if (manifest.environments?.list?.[environment]) {
79
+ throw new errors_js_1.CliError(`Environment "${environment}" already exists. Use "slskit configure --env ${environment}" to change it.`);
80
+ }
81
+ assertNamesFit(manifest, environment);
82
+ await (0, action_js_1.configureAction)({ ...options, env: environment });
83
+ }
84
+ async function envUseAction(name) {
85
+ const cwd = process.cwd();
86
+ const manifest = (0, environments_js_1.readManifest)(cwd, "slskit env use");
87
+ const environment = requireName(name);
88
+ (0, environments_js_1.requireEnvironment)(manifest, environment);
89
+ (0, environments_js_1.writeManifest)(cwd, {
90
+ ...manifest,
91
+ environments: {
92
+ default: environment,
93
+ list: manifest.environments?.list ?? {},
94
+ },
95
+ });
96
+ logger_js_1.logger.info(`\nDefault environment is now "${environment}".`);
97
+ logger_js_1.logger.info(` ${environments_js_1.APP_ENVIRONMENT_KEY}=${environment}`);
98
+ }
99
+ async function envRemoveAction(name, options) {
100
+ const cwd = process.cwd();
101
+ const manifest = (0, environments_js_1.readManifest)(cwd, "slskit env remove");
102
+ const environment = requireName(name);
103
+ const config = (0, environments_js_1.requireEnvironment)(manifest, environment);
104
+ const remaining = (0, environments_js_1.environmentNames)(manifest).filter((each) => each !== environment);
105
+ if (!options.yes) {
106
+ if (!(0, is_ci_js_1.isInteractive)()) {
107
+ throw new errors_js_1.CliError(`Removing an environment cannot be undone. Re-run with --yes to remove "${environment}".`);
108
+ }
109
+ const { confirm } = await import("@inquirer/prompts");
110
+ const count = Object.keys(config.variables ?? {}).length;
111
+ const proceed = await confirm({
112
+ message: `Remove environment "${environment}" and its ${count} variable(s) from slskit.json?`,
113
+ default: false,
114
+ });
115
+ if (!proceed) {
116
+ logger_js_1.logger.info("\nNothing was removed.");
117
+ return;
118
+ }
119
+ }
120
+ const list = { ...manifest.environments?.list };
121
+ delete list[environment];
122
+ const wasDefault = manifest.environments?.default === environment;
123
+ (0, environments_js_1.writeManifest)(cwd, {
124
+ ...manifest,
125
+ environments: {
126
+ default: wasDefault ? remaining[0] ?? environment : manifest.environments.default,
127
+ list,
128
+ },
129
+ });
130
+ const written = (0, templates_js_1.regenerateTemplates)(cwd, (0, environments_js_1.readManifest)(cwd, "slskit env remove"));
131
+ logger_js_1.logger.info(`\nRemoved environment "${environment}".`);
132
+ if (wasDefault && remaining.length > 0) {
133
+ logger_js_1.logger.info(` default environment is now "${remaining[0]}"`);
134
+ }
135
+ logger_js_1.logger.info(` ${(0, dotenv_js_1.dotenvFileName)(environment)} was left in place — delete it yourself if you no longer need those values.`);
136
+ if (written.length > 0) {
137
+ logger_js_1.logger.info(` regenerated ${written.length} template(s)`);
138
+ }
139
+ }
140
+ async function envSetAction(assignment, options) {
141
+ const cwd = process.cwd();
142
+ const manifest = (0, environments_js_1.readManifest)(cwd, "slskit env set");
143
+ const environment = (0, environments_js_1.resolveEnvironmentName)(manifest, options.env);
144
+ const config = (0, environments_js_1.requireEnvironment)(manifest, environment);
145
+ const eq = assignment.indexOf("=");
146
+ const hasValue = eq > 0;
147
+ const key = parseKey(hasValue ? assignment.slice(0, eq) : assignment);
148
+ const value = hasValue ? assignment.slice(eq + 1) : undefined;
149
+ if (options.ssm && options.secret) {
150
+ throw new errors_js_1.CliError('Pass either "--secret" or "--ssm", not both.');
151
+ }
152
+ let def;
153
+ let where;
154
+ if (options.ssm) {
155
+ def = { ssm: options.ssm };
156
+ where = `SSM parameter ${options.ssm}`;
157
+ }
158
+ else if (options.secret) {
159
+ if (value === undefined) {
160
+ throw new errors_js_1.CliError(`A secret needs a value: slskit env set ${key}=<value> --secret`);
161
+ }
162
+ def = { secret: true };
163
+ (0, dotenv_js_1.ensureDotenvIgnored)(cwd);
164
+ const file = (0, dotenv_js_1.setDotenvValue)(cwd, environment, key, value);
165
+ where = file;
166
+ }
167
+ else {
168
+ if (value === undefined) {
169
+ throw new errors_js_1.CliError(`A value is required: slskit env set ${key}=<value>`);
170
+ }
171
+ def = { value };
172
+ where = "slskit.json";
173
+ }
174
+ const variables = { ...config.variables, [key]: def };
175
+ const knownBefore = Object.keys(config.variables ?? {});
176
+ (0, environments_js_1.writeManifest)(cwd, {
177
+ ...manifest,
178
+ environments: {
179
+ default: manifest.environments.default,
180
+ list: {
181
+ ...manifest.environments.list,
182
+ [environment]: { ...config, variables },
183
+ },
184
+ },
185
+ });
186
+ logger_js_1.logger.info(`\nSet ${key} for environment "${environment}".`);
187
+ logger_js_1.logger.info(` stored in: ${where}`);
188
+ // A brand-new variable name means every template needs a matching parameter.
189
+ if (!knownBefore.includes(key)) {
190
+ const written = (0, templates_js_1.regenerateTemplates)(cwd, (0, environments_js_1.readManifest)(cwd, "slskit env set"));
191
+ if (written.length > 0) {
192
+ logger_js_1.logger.info(` regenerated ${written.length} template(s) for the new parameter`);
193
+ }
194
+ }
195
+ }
196
+ async function envUnsetAction(key, options) {
197
+ const cwd = process.cwd();
198
+ const manifest = (0, environments_js_1.readManifest)(cwd, "slskit env unset");
199
+ const environment = (0, environments_js_1.resolveEnvironmentName)(manifest, options.env);
200
+ const config = (0, environments_js_1.requireEnvironment)(manifest, environment);
201
+ const name = parseKey(key);
202
+ if (!config.variables?.[name]) {
203
+ throw new errors_js_1.CliError(`Variable "${name}" is not set for environment "${environment}".`);
204
+ }
205
+ const variables = { ...config.variables };
206
+ delete variables[name];
207
+ (0, environments_js_1.writeManifest)(cwd, {
208
+ ...manifest,
209
+ environments: {
210
+ default: manifest.environments.default,
211
+ list: {
212
+ ...manifest.environments.list,
213
+ [environment]: { ...config, variables },
214
+ },
215
+ },
216
+ });
217
+ (0, dotenv_js_1.unsetDotenvValue)(cwd, environment, name);
218
+ const written = (0, templates_js_1.regenerateTemplates)(cwd, (0, environments_js_1.readManifest)(cwd, "slskit env unset"));
219
+ logger_js_1.logger.info(`\nRemoved ${name} from environment "${environment}".`);
220
+ if (written.length > 0) {
221
+ logger_js_1.logger.info(` regenerated ${written.length} template(s)`);
222
+ }
223
+ }
224
+ async function envVarsAction(options) {
225
+ const cwd = process.cwd();
226
+ const manifest = (0, environments_js_1.readManifest)(cwd, "slskit env vars");
227
+ const environment = (0, environments_js_1.resolveEnvironmentName)(manifest, options.env);
228
+ const config = (0, environments_js_1.requireEnvironment)(manifest, environment);
229
+ const secrets = (0, dotenv_js_1.readDotenv)(cwd, environment);
230
+ logger_js_1.logger.info(`\nVariables for environment "${environment}":\n`);
231
+ // Always shown first: it is generated, not stored, and every name derives from it.
232
+ logger_js_1.logger.info(` ${environments_js_1.APP_ENVIRONMENT_KEY}=${environment} (generated)`);
233
+ // Declared and undeclared together: a key typed straight into the .env file
234
+ // reaches the functions exactly like one added with "slskit env set".
235
+ const keys = (0, keys_js_1.environmentEnvKeys)(cwd, manifest, environment);
236
+ for (const key of keys) {
237
+ const def = config.variables?.[key];
238
+ if (!def) {
239
+ const held = secrets[key] ?? "";
240
+ const shown = options.showSecrets || held === "" ? held : "********";
241
+ logger_js_1.logger.info(` ${key}=${shown} (${(0, dotenv_js_1.dotenvFileName)(environment)})`);
242
+ continue;
243
+ }
244
+ const source = (0, environments_js_1.envVarSource)(def);
245
+ if (source === "ssm") {
246
+ logger_js_1.logger.info(` ${key}=<ssm:${def.ssm}> (resolved by AWS at deploy)`);
247
+ continue;
248
+ }
249
+ if (source === "secret") {
250
+ const held = secrets[key];
251
+ const shown = options.showSecrets
252
+ ? held ?? "(missing)"
253
+ : held
254
+ ? "********"
255
+ : "(missing)";
256
+ logger_js_1.logger.info(` ${key}=${shown} (secret, ${(0, dotenv_js_1.dotenvFileName)(environment)})`);
257
+ continue;
258
+ }
259
+ logger_js_1.logger.info(` ${key}=${def.value ?? ""} (slskit.json)`);
260
+ }
261
+ const hidden = keys.some((key) => config.variables?.[key]?.secret || config.variables?.[key] === undefined);
262
+ if (keys.length === 0) {
263
+ logger_js_1.logger.info(`\n No variables set yet. Add one with "slskit env set KEY=value", or put it straight into ${(0, dotenv_js_1.dotenvFileName)(environment)}.`);
264
+ }
265
+ else if (!options.showSecrets && hidden) {
266
+ logger_js_1.logger.info("\nRe-run with --show-secrets to reveal hidden values.");
267
+ }
268
+ }
269
+ // Called by init and configure so a new environment always has its dotenv file.
270
+ function seedDotenv(cwd, environment) {
271
+ (0, dotenv_js_1.ensureDotenvIgnored)(cwd);
272
+ const existing = (0, dotenv_js_1.readDotenv)(cwd, environment);
273
+ return (0, dotenv_js_1.writeDotenv)(cwd, environment, existing);
274
+ }
@@ -0,0 +1,6 @@
1
+ export declare function dotenvFileName(environment: string): string;
2
+ export declare function readDotenv(cwd: string, environment: string): Record<string, string>;
3
+ export declare function writeDotenv(cwd: string, environment: string, values: Record<string, string>): string;
4
+ export declare function setDotenvValue(cwd: string, environment: string, key: string, value: string): string;
5
+ export declare function unsetDotenvValue(cwd: string, environment: string, key: string): void;
6
+ export declare function ensureDotenvIgnored(cwd: string): boolean;
@@ -0,0 +1,99 @@
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.dotenvFileName = dotenvFileName;
7
+ exports.readDotenv = readDotenv;
8
+ exports.writeDotenv = writeDotenv;
9
+ exports.setDotenvValue = setDotenvValue;
10
+ exports.unsetDotenvValue = unsetDotenvValue;
11
+ exports.ensureDotenvIgnored = ensureDotenvIgnored;
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const environments_js_1 = require("../../core/environments.js");
15
+ // Secret values live beside the project in .env.<environment>, which the generated
16
+ // .gitignore excludes. Nothing in this file is ever written into slskit.json.
17
+ function dotenvFileName(environment) {
18
+ return `.env.${environment}`;
19
+ }
20
+ function parseLine(line) {
21
+ const trimmed = line.trim();
22
+ if (!trimmed || trimmed.startsWith("#")) {
23
+ return undefined;
24
+ }
25
+ const eq = trimmed.indexOf("=");
26
+ if (eq <= 0) {
27
+ return undefined;
28
+ }
29
+ const key = trimmed.slice(0, eq).trim();
30
+ let value = trimmed.slice(eq + 1).trim();
31
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
32
+ value = value.slice(1, -1).replace(/\\(["\\])/g, "$1");
33
+ }
34
+ else if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
35
+ value = value.slice(1, -1);
36
+ }
37
+ return [key, value];
38
+ }
39
+ function readDotenv(cwd, environment) {
40
+ const file = node_path_1.default.join(cwd, dotenvFileName(environment));
41
+ if (!node_fs_1.default.existsSync(file)) {
42
+ return {};
43
+ }
44
+ const values = {};
45
+ for (const line of node_fs_1.default.readFileSync(file, "utf8").split("\n")) {
46
+ const entry = parseLine(line);
47
+ if (entry) {
48
+ values[entry[0]] = entry[1];
49
+ }
50
+ }
51
+ return values;
52
+ }
53
+ // Values are quoted when they contain anything that would not survive a raw round trip.
54
+ function formatLine(key, value) {
55
+ if (/^[A-Za-z0-9_./:-]*$/.test(value)) {
56
+ return `${key}=${value}`;
57
+ }
58
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
59
+ return `${key}="${escaped}"`;
60
+ }
61
+ // APP_ENVIRONMENT is always written first and always equals the environment name,
62
+ // so the file is self-describing and can never disagree with the manifest.
63
+ function writeDotenv(cwd, environment, values) {
64
+ const file = dotenvFileName(environment);
65
+ const rest = Object.keys(values)
66
+ .filter((key) => key !== environments_js_1.APP_ENVIRONMENT_KEY)
67
+ .sort()
68
+ .map((key) => formatLine(key, values[key]));
69
+ const body = [formatLine(environments_js_1.APP_ENVIRONMENT_KEY, environment), ...rest].join("\n");
70
+ const header = `# Values for the "${environment}" environment. Never commit this file.\n`;
71
+ node_fs_1.default.writeFileSync(node_path_1.default.join(cwd, file), `${header}${body}\n`);
72
+ return file;
73
+ }
74
+ function setDotenvValue(cwd, environment, key, value) {
75
+ const values = readDotenv(cwd, environment);
76
+ values[key] = value;
77
+ return writeDotenv(cwd, environment, values);
78
+ }
79
+ function unsetDotenvValue(cwd, environment, key) {
80
+ const values = readDotenv(cwd, environment);
81
+ if (!(key in values)) {
82
+ return;
83
+ }
84
+ delete values[key];
85
+ writeDotenv(cwd, environment, values);
86
+ }
87
+ // A leaked credential is the expensive failure here, so the ignore rule is repaired
88
+ // on every write rather than trusted to whatever the project was scaffolded with.
89
+ function ensureDotenvIgnored(cwd) {
90
+ const file = node_path_1.default.join(cwd, ".gitignore");
91
+ const existing = node_fs_1.default.existsSync(file) ? node_fs_1.default.readFileSync(file, "utf8") : "";
92
+ const lines = existing.split("\n").map((line) => line.trim());
93
+ if (lines.includes(".env.*")) {
94
+ return false;
95
+ }
96
+ const separator = existing.endsWith("\n") || existing === "" ? "" : "\n";
97
+ node_fs_1.default.writeFileSync(file, `${existing}${separator}.env\n.env.*\n`);
98
+ return true;
99
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerEnvCommand(program: Command): void;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerEnvCommand = registerEnvCommand;
4
+ const run_js_1 = require("../../core/run.js");
5
+ function registerEnvCommand(program) {
6
+ const env = program
7
+ .command("env")
8
+ .description("Manage deployment environments (dev, staging, production, ...) and their variables");
9
+ env
10
+ .command("list")
11
+ .alias("ls")
12
+ .description("Show every environment and its deploy target")
13
+ .action(() => (0, run_js_1.runCommand)(async () => {
14
+ const { envListAction } = await import("./action.js");
15
+ await envListAction();
16
+ }));
17
+ env
18
+ .command("add")
19
+ .description("Add an environment and configure its AWS deploy target")
20
+ .argument("<name>", "Environment name (example: staging)")
21
+ .option("--profile <name>", "AWS named profile to resolve credentials from")
22
+ .option("--region <region>", "AWS region to deploy into (example: us-east-1)")
23
+ .option("--stack-name <name>", "CloudFormation stack name for this environment")
24
+ .option("--skip-verify", "Save without checking that the credentials work", false)
25
+ .action((name, options) => (0, run_js_1.runCommand)(async () => {
26
+ const { envAddAction } = await import("./action.js");
27
+ await envAddAction(name, options);
28
+ }));
29
+ env
30
+ .command("use")
31
+ .description("Set the environment that commands default to")
32
+ .argument("<name>", "Environment name")
33
+ .action((name) => (0, run_js_1.runCommand)(async () => {
34
+ const { envUseAction } = await import("./action.js");
35
+ await envUseAction(name);
36
+ }));
37
+ env
38
+ .command("remove")
39
+ .alias("rm")
40
+ .description("Remove an environment from this project")
41
+ .argument("<name>", "Environment name")
42
+ .option("-y, --yes", "Skip the confirmation prompt", false)
43
+ .action((name, options) => (0, run_js_1.runCommand)(async () => {
44
+ const { envRemoveAction } = await import("./action.js");
45
+ await envRemoveAction(name, options);
46
+ }));
47
+ env
48
+ .command("set")
49
+ .description("Add or update a variable (KEY=value)")
50
+ .argument("<assignment>", "KEY=value")
51
+ .option("-e, --env <name>", "Environment to change (default: the default environment)")
52
+ .option("--secret", "Store the value in .env.<environment> instead of slskit.json", false)
53
+ .option("--ssm <path>", "Read the value from an SSM Parameter Store path at deploy time")
54
+ .action((assignment, options) => (0, run_js_1.runCommand)(async () => {
55
+ const { envSetAction } = await import("./action.js");
56
+ await envSetAction(assignment, options);
57
+ }));
58
+ env
59
+ .command("unset")
60
+ .description("Remove a variable from an environment")
61
+ .argument("<key>", "Variable name")
62
+ .option("-e, --env <name>", "Environment to change (default: the default environment)")
63
+ .action((key, options) => (0, run_js_1.runCommand)(async () => {
64
+ const { envUnsetAction } = await import("./action.js");
65
+ await envUnsetAction(key, options);
66
+ }));
67
+ env
68
+ .command("vars")
69
+ .description("Show every variable configured for an environment")
70
+ .option("-e, --env <name>", "Environment to show (default: the default environment)")
71
+ .option("--show-secrets", "Reveal secret values instead of masking them", false)
72
+ .action((options) => (0, run_js_1.runCommand)(async () => {
73
+ const { envVarsAction } = await import("./action.js");
74
+ await envVarsAction(options);
75
+ }));
76
+ }
@@ -0,0 +1,8 @@
1
+ import type { EnvironmentsConfig } from "../../core/environments.js";
2
+ type ManifestView = {
3
+ environments?: EnvironmentsConfig;
4
+ };
5
+ export declare function isVariableName(key: string): boolean;
6
+ export declare function projectEnvKeys(cwd: string, manifest: ManifestView): string[];
7
+ export declare function environmentEnvKeys(cwd: string, manifest: ManifestView, environment: string): string[];
8
+ export {};
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isVariableName = isVariableName;
4
+ exports.projectEnvKeys = projectEnvKeys;
5
+ exports.environmentEnvKeys = environmentEnvKeys;
6
+ const environments_js_1 = require("../../core/environments.js");
7
+ const dotenv_js_1 = require("./dotenv.js");
8
+ // Anything that is not a legal shell/Lambda variable name is skipped rather than
9
+ // turned into a CloudFormation parameter, so a stray line in a hand-edited .env
10
+ // file can never produce an unbuildable template.
11
+ const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
12
+ function isVariableName(key) {
13
+ return VARIABLE_NAME.test(key) && key !== environments_js_1.APP_ENVIRONMENT_KEY;
14
+ }
15
+ // Every variable name in play across the whole project, whether it was declared
16
+ // with "slskit env set" or simply typed into a .env.<environment> file by hand.
17
+ // Templates are shared by every environment, so they declare a parameter for the
18
+ // union: a variable only one stage sets still has to exist in the template that
19
+ // every other stage deploys from.
20
+ function projectEnvKeys(cwd, manifest) {
21
+ const keys = new Set((0, environments_js_1.allEnvKeys)(manifest).filter(isVariableName));
22
+ for (const environment of Object.keys(manifest.environments?.list ?? {})) {
23
+ for (const key of Object.keys((0, dotenv_js_1.readDotenv)(cwd, environment))) {
24
+ if (isVariableName(key)) {
25
+ keys.add(key);
26
+ }
27
+ }
28
+ }
29
+ return [...keys].sort();
30
+ }
31
+ // The keys one environment actually supplies a value for, declared or not.
32
+ function environmentEnvKeys(cwd, manifest, environment) {
33
+ const config = manifest.environments?.list?.[environment];
34
+ const keys = new Set(Object.keys(config?.variables ?? {}).filter(isVariableName));
35
+ for (const key of Object.keys((0, dotenv_js_1.readDotenv)(cwd, environment))) {
36
+ if (isVariableName(key)) {
37
+ keys.add(key);
38
+ }
39
+ }
40
+ return [...keys].sort();
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { ProjectManifest } from "../../core/environments.js";
2
+ export interface ParameterOverride {
3
+ name: string;
4
+ value: string;
5
+ }
6
+ export declare function parameterOverrides(cwd: string, manifest: ProjectManifest, environment: string): ParameterOverride[];
7
+ export declare function toCliArguments(overrides: ParameterOverride[]): string[];
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parameterOverrides = parameterOverrides;
4
+ exports.toCliArguments = toCliArguments;
5
+ const errors_js_1 = require("../../core/errors.js");
6
+ const environments_js_1 = require("../../core/environments.js");
7
+ const helpers_js_1 = require("../init/templates/helpers.js");
8
+ const dotenv_js_1 = require("./dotenv.js");
9
+ const keys_js_1 = require("./keys.js");
10
+ // Turns one environment into the --parameter-overrides list that both "slskit run"
11
+ // and a deploy hand to the AWS SAM CLI. AppEnvironment always comes first: it is
12
+ // what every stage-scoped resource name is built from.
13
+ function parameterOverrides(cwd, manifest, environment) {
14
+ const config = (0, environments_js_1.requireEnvironment)(manifest, environment);
15
+ const overrides = [
16
+ { name: environments_js_1.APP_ENVIRONMENT_PARAM, value: environment },
17
+ ];
18
+ const fileValues = (0, dotenv_js_1.readDotenv)(cwd, environment);
19
+ const declared = config.variables ?? {};
20
+ // The whole project's key set, not just this environment's: templates are shared,
21
+ // so every parameter they declare has to be resolvable from here.
22
+ for (const key of (0, keys_js_1.projectEnvKeys)(cwd, manifest)) {
23
+ const def = declared[key];
24
+ const name = (0, helpers_js_1.envParameterName)(key);
25
+ if (!def) {
26
+ // Undeclared: it exists only because someone put it in a .env file. Stages
27
+ // that leave it out fall through to the parameter's empty default.
28
+ const value = fileValues[key];
29
+ if (value !== undefined && value !== "") {
30
+ overrides.push({ name, value });
31
+ }
32
+ continue;
33
+ }
34
+ const source = (0, environments_js_1.envVarSource)(def);
35
+ if (source === "ssm") {
36
+ // A CloudFormation dynamic reference: AWS resolves it at deploy time, so the
37
+ // secret never passes through this process.
38
+ overrides.push({ name, value: `{{resolve:ssm:${def.ssm}}}` });
39
+ continue;
40
+ }
41
+ if (source === "secret") {
42
+ const value = fileValues[key];
43
+ if (value === undefined) {
44
+ throw new errors_js_1.CliError(`Variable "${key}" is marked secret for environment "${environment}" but is missing from ${(0, dotenv_js_1.dotenvFileName)(environment)}.\nSet it with: slskit env set ${key}=<value> --secret --env ${environment}`);
45
+ }
46
+ overrides.push({ name, value });
47
+ continue;
48
+ }
49
+ overrides.push({ name, value: def.value ?? "" });
50
+ }
51
+ return overrides;
52
+ }
53
+ // The shorthand "Key=Value" form the AWS SAM CLI also accepts splits on whitespace,
54
+ // so a value with a space in it is silently truncated -- "hello world" arrives as
55
+ // "hello". The explicit ParameterKey/ParameterValue form with the value quoted is the
56
+ // only one that survives spaces, commas and equals signs alike. Backslashes are
57
+ // deliberately left alone: escaping them doubles them.
58
+ function quoteValue(value) {
59
+ return `"${value.replace(/"/g, '\\"')}"`;
60
+ }
61
+ function toCliArguments(overrides) {
62
+ return overrides.map(({ name, value }) => `ParameterKey=${name},ParameterValue=${quoteValue(value)}`);
63
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectManifest } from "../../core/environments.js";
2
+ export declare function regenerateTemplates(cwd: string, manifest: ProjectManifest): string[];
3
+ export declare function syncTemplates(cwd: string, manifest: ProjectManifest): string[];
@@ -0,0 +1,65 @@
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.regenerateTemplates = regenerateTemplates;
7
+ exports.syncTemplates = syncTemplates;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const helpers_js_1 = require("../init/templates/helpers.js");
11
+ const types_js_1 = require("../init/types.js");
12
+ const sam_js_1 = require("../init/templates/sam.js");
13
+ const manifest_js_1 = require("../function/manifest.js");
14
+ const keys_js_1 = require("./keys.js");
15
+ // Templates declare a CloudFormation parameter per variable name, so every template
16
+ // has to be rewritten whenever that set changes in any environment.
17
+ function regenerateTemplates(cwd, manifest) {
18
+ const functionView = manifest;
19
+ if (!Array.isArray(functionView.applications)) {
20
+ return [];
21
+ }
22
+ const answers = (0, manifest_js_1.toInitAnswers)(functionView);
23
+ const apps = (0, manifest_js_1.toServiceDefs)(functionView);
24
+ const envKeys = (0, keys_js_1.projectEnvKeys)(cwd, manifest);
25
+ const written = [];
26
+ for (const app of apps) {
27
+ const relative = (0, types_js_1.serviceTemplatePath)(app.name);
28
+ const full = node_path_1.default.join(cwd, relative);
29
+ // Absent in a project generated before services had their own templates.
30
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(full), { recursive: true });
31
+ node_fs_1.default.writeFileSync(full, (0, sam_js_1.samServiceTemplate)(answers, app, envKeys));
32
+ written.push(relative);
33
+ }
34
+ node_fs_1.default.writeFileSync(node_path_1.default.join(cwd, "template.yaml"), (0, sam_js_1.samRootTemplate)(answers, apps, envKeys));
35
+ written.push("template.yaml");
36
+ return written;
37
+ }
38
+ // The stage parameters the root template currently declares. Scoped to the
39
+ // Parameters block so a resource that happens to start with "Env" is never counted.
40
+ function declaredStageParameters(cwd) {
41
+ const file = node_path_1.default.join(cwd, "template.yaml");
42
+ if (!node_fs_1.default.existsSync(file)) {
43
+ return new Set();
44
+ }
45
+ const body = node_fs_1.default.readFileSync(file, "utf8");
46
+ const block = /^Parameters:\n((?: .*\n|\n)*)/m.exec(body);
47
+ if (!block) {
48
+ return new Set();
49
+ }
50
+ const names = new Set();
51
+ for (const match of block[1].matchAll(/^ {2}(Env[A-Za-z0-9]*):$/gm)) {
52
+ names.add(match[1]);
53
+ }
54
+ return names;
55
+ }
56
+ // Variables can appear without any slskit command running -- someone edits
57
+ // .env.production by hand -- so "run" and "deploy" reconcile the templates with the
58
+ // current key set before handing anything to SAM. Returns the files it rewrote,
59
+ // empty when they were already in step.
60
+ function syncTemplates(cwd, manifest) {
61
+ const wanted = new Set((0, keys_js_1.projectEnvKeys)(cwd, manifest).map(helpers_js_1.envParameterName));
62
+ const declared = declaredStageParameters(cwd);
63
+ const inStep = wanted.size === declared.size && [...wanted].every((name) => declared.has(name));
64
+ return inStep ? [] : regenerateTemplates(cwd, manifest);
65
+ }