@lunora/config 0.0.0 → 1.0.0-alpha.2

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 (39) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +115 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/index.d.mts +1075 -0
  5. package/dist/index.d.ts +1075 -0
  6. package/dist/index.mjs +20 -0
  7. package/dist/packem_shared/AGENT_RULES_DIR-lcgC08aE.mjs +40 -0
  8. package/dist/packem_shared/DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs +37 -0
  9. package/dist/packem_shared/LINKED_PROJECT_DIR-CXwXzV_C.mjs +52 -0
  10. package/dist/packem_shared/PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs +62 -0
  11. package/dist/packem_shared/REQUIRED_COMPATIBILITY_DATE-Dd1suoit.mjs +476 -0
  12. package/dist/packem_shared/applyAdditiveEdit-C-snTFEV.mjs +228 -0
  13. package/dist/packem_shared/buildPackageSecretsBlock-S74dgmwy.mjs +187 -0
  14. package/dist/packem_shared/classifyPolicyEdit-BHeAqF8P.mjs +99 -0
  15. package/dist/packem_shared/createConfirm-fvpdgJ9s.mjs +100 -0
  16. package/dist/packem_shared/detectFramework-Br-BcPBq.mjs +41 -0
  17. package/dist/packem_shared/discoverContainerInfo-BXFs6Wav.mjs +19 -0
  18. package/dist/packem_shared/discoverSchemaInfo-DWtypqpP.mjs +25 -0
  19. package/dist/packem_shared/discoverWorkflowInfo-CedvR0mn.mjs +19 -0
  20. package/dist/packem_shared/findWranglerFile-DwSuC-Kn.mjs +25 -0
  21. package/dist/packem_shared/formatLunoraEvent-D2fDeGB6.mjs +86 -0
  22. package/dist/packem_shared/handlePolicyScaffoldRequest-CiC2IGKx.mjs +103 -0
  23. package/dist/packem_shared/handleSchemaEditRequest-Df-Wrix-.mjs +99 -0
  24. package/dist/packem_shared/handleSeedRequest-DVCjaGO-.mjs +61 -0
  25. package/dist/packem_shared/inferLunoraBindings-0W3eRdIP.mjs +302 -0
  26. package/dist/packem_shared/injectRemoteFlags-C-WZAKLY.mjs +105 -0
  27. package/dist/packem_shared/interpretRemote-CtcIcB5-.mjs +34 -0
  28. package/dist/packem_shared/parseDevVariable-CJiq2IwE.mjs +30 -0
  29. package/dist/packem_shared/parseSchema-DSeyktvG.mjs +107 -0
  30. package/dist/packem_shared/policy-scaffold.d-DCmwn7zQ.d.mts +74 -0
  31. package/dist/packem_shared/policy-scaffold.d-DCmwn7zQ.d.ts +74 -0
  32. package/dist/packem_shared/reconcileWranglerBindings-ByJk3yLU.mjs +277 -0
  33. package/dist/packem_shared/renderStudioHtml-449Ysn75.mjs +37 -0
  34. package/dist/packem_shared/serveJsonHandler-B4OLTGLS.mjs +86 -0
  35. package/dist/packem_shared/studioAssetsStamp-Csk5RS4E.mjs +28 -0
  36. package/dist/studio-host/index.d.mts +227 -0
  37. package/dist/studio-host/index.d.ts +227 -0
  38. package/dist/studio-host/index.mjs +7 -0
  39. package/package.json +57 -17
@@ -0,0 +1,228 @@
1
+ import { Project, SyntaxKind } from 'ts-morph';
2
+ import { collectCalls } from './parseSchema-DSeyktvG.mjs';
3
+
4
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/u;
5
+ const VALIDATOR_METHODS = /* @__PURE__ */ new Set([
6
+ "any",
7
+ "array",
8
+ "bigint",
9
+ "boolean",
10
+ "bytes",
11
+ "date",
12
+ "id",
13
+ "literal",
14
+ "null",
15
+ "number",
16
+ "object",
17
+ "optional",
18
+ "record",
19
+ "storage",
20
+ "string",
21
+ "timestamp",
22
+ "union"
23
+ ]);
24
+ const isValidatorExpression = (node) => {
25
+ if (node.getKind() === SyntaxKind.ParenthesizedExpression) {
26
+ return isValidatorExpression(node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression());
27
+ }
28
+ if (node.getKind() !== SyntaxKind.CallExpression) {
29
+ return false;
30
+ }
31
+ const call = node.asKindOrThrow(SyntaxKind.CallExpression);
32
+ const callee = call.getExpression();
33
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression) {
34
+ return false;
35
+ }
36
+ const access = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
37
+ if (access.getExpression().getKind() !== SyntaxKind.Identifier || access.getExpression().getText() !== "v" || !VALIDATOR_METHODS.has(access.getName())) {
38
+ return false;
39
+ }
40
+ return call.getArguments().every((argument) => isValidatorArgument(argument));
41
+ };
42
+ const isValidatorArgument = (node) => {
43
+ const kind = node.getKind();
44
+ if (kind === SyntaxKind.ParenthesizedExpression) {
45
+ return isValidatorArgument(node.asKindOrThrow(SyntaxKind.ParenthesizedExpression).getExpression());
46
+ }
47
+ if (kind === SyntaxKind.StringLiteral || kind === SyntaxKind.NumericLiteral || kind === SyntaxKind.TrueKeyword || kind === SyntaxKind.FalseKeyword || kind === SyntaxKind.NullKeyword) {
48
+ return true;
49
+ }
50
+ if (kind === SyntaxKind.PrefixUnaryExpression) {
51
+ const unary = node.asKindOrThrow(SyntaxKind.PrefixUnaryExpression);
52
+ return unary.getOperand().getKind() === SyntaxKind.NumericLiteral;
53
+ }
54
+ if (kind === SyntaxKind.ObjectLiteralExpression) {
55
+ return node.asKindOrThrow(SyntaxKind.ObjectLiteralExpression).getProperties().every((property) => {
56
+ if (property.getKind() !== SyntaxKind.PropertyAssignment) {
57
+ return false;
58
+ }
59
+ const initializer = property.getInitializer();
60
+ return initializer !== void 0 && isValidatorArgument(initializer);
61
+ });
62
+ }
63
+ if (kind === SyntaxKind.ArrayLiteralExpression) {
64
+ return node.asKindOrThrow(SyntaxKind.ArrayLiteralExpression).getElements().every((element) => isValidatorArgument(element));
65
+ }
66
+ return isValidatorExpression(node);
67
+ };
68
+ const isAllowedValidatorText = (validator) => {
69
+ if (typeof validator !== "string" || validator.trim().length === 0) {
70
+ return false;
71
+ }
72
+ const project = new Project({ compilerOptions: { allowJs: true }, useInMemoryFileSystem: true });
73
+ const sourceFile = project.createSourceFile("validator.ts", `const __v = (${validator});`, { overwrite: true });
74
+ if (project.getProgram().getSyntacticDiagnostics(sourceFile).length > 0) {
75
+ return false;
76
+ }
77
+ if (sourceFile.getStatements().length !== 1) {
78
+ return false;
79
+ }
80
+ const declaration = sourceFile.getVariableDeclaration("__v");
81
+ const initializer = declaration?.getInitializer();
82
+ return initializer !== void 0 && isValidatorExpression(initializer);
83
+ };
84
+ const ADDITIVE_KINDS = /* @__PURE__ */ new Set(["addIndex", "addOptionalColumn", "addTable"]);
85
+ const classifyEdit = (edit) => ADDITIVE_KINDS.has(edit.kind) ? "additive" : "destructive";
86
+ const isIdentifier = (value) => typeof value === "string" && IDENTIFIER_PATTERN.test(value);
87
+ const validateAdditiveEdit = (edit) => {
88
+ if (!isIdentifier(edit.table)) {
89
+ return "invalid-identifier";
90
+ }
91
+ if (edit.kind === "addOptionalColumn") {
92
+ if (!isIdentifier(edit.column)) {
93
+ return "invalid-identifier";
94
+ }
95
+ if (!isAllowedValidatorText(edit.validator)) {
96
+ return "invalid-validator";
97
+ }
98
+ }
99
+ if (edit.kind === "addIndex") {
100
+ if (!isIdentifier(edit.name)) {
101
+ return "invalid-identifier";
102
+ }
103
+ if (!Array.isArray(edit.fields) || !edit.fields.every((field) => isIdentifier(field))) {
104
+ return "invalid-identifier";
105
+ }
106
+ }
107
+ return void 0;
108
+ };
109
+ const findTablesObject = (source) => {
110
+ const project = new Project({ compilerOptions: { allowJs: true }, useInMemoryFileSystem: true });
111
+ const sourceFile = project.createSourceFile("schema.ts", source, { overwrite: true });
112
+ let defineSchemaCall;
113
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
114
+ if (call.getExpression().getText() === "defineSchema") {
115
+ defineSchemaCall = call;
116
+ break;
117
+ }
118
+ }
119
+ if (defineSchemaCall === void 0) {
120
+ const aliased = sourceFile.getImportDeclarations().some((declaration) => declaration.getNamedImports().some((named) => named.getName() === "defineSchema" && named.getAliasNode() !== void 0));
121
+ return { reason: aliased ? "aliased-define-schema" : "no-define-schema" };
122
+ }
123
+ const tablesArgument = defineSchemaCall.getArguments()[0];
124
+ if (tablesArgument?.getKind() !== SyntaxKind.ObjectLiteralExpression) {
125
+ return { reason: "non-object-argument" };
126
+ }
127
+ return { object: tablesArgument.asKindOrThrow(SyntaxKind.ObjectLiteralExpression), sourceFile };
128
+ };
129
+ const findTableProperty = (tablesObject, table) => {
130
+ for (const property of tablesObject.getProperties()) {
131
+ if (property.getKind() === SyntaxKind.PropertyAssignment && property.getNameNode().getText().replaceAll(/["']/gu, "") === table) {
132
+ return property;
133
+ }
134
+ }
135
+ return void 0;
136
+ };
137
+ const findDefineTableShape = (tableProperty) => {
138
+ const defineTableCall = tableProperty.getInitializer()?.getDescendantsOfKind(SyntaxKind.CallExpression).find((call) => call.getExpression().getText() === "defineTable");
139
+ const shape = defineTableCall?.getArguments()[0];
140
+ return shape?.getKind() === SyntaxKind.ObjectLiteralExpression ? shape.asKindOrThrow(SyntaxKind.ObjectLiteralExpression) : void 0;
141
+ };
142
+ const applyAddTable = (tablesObject, edit) => {
143
+ if (findTableProperty(tablesObject, edit.table) !== void 0) {
144
+ return "duplicate-table";
145
+ }
146
+ tablesObject.addPropertyAssignment({
147
+ initializer: `defineTable({
148
+ // Add your column validators here.
149
+ // Example:
150
+ // text: v.string(),
151
+ // createdAt: v.number(),
152
+ })`,
153
+ name: edit.table
154
+ });
155
+ return void 0;
156
+ };
157
+ const applyAddOptionalColumn = (tablesObject, edit) => {
158
+ const tableProperty = findTableProperty(tablesObject, edit.table);
159
+ if (tableProperty === void 0) {
160
+ return "unknown-table";
161
+ }
162
+ const shape = findDefineTableShape(tableProperty);
163
+ if (shape === void 0) {
164
+ return "unknown-table";
165
+ }
166
+ const exists = shape.getProperties().some(
167
+ (property) => property.getKind() === SyntaxKind.PropertyAssignment && property.getNameNode().getText().replaceAll(/["']/gu, "") === edit.column
168
+ );
169
+ if (exists) {
170
+ return "duplicate-column";
171
+ }
172
+ shape.addPropertyAssignment({ initializer: `v.optional(${edit.validator})`, name: edit.column });
173
+ return void 0;
174
+ };
175
+ const applyAddIndex = (tableProperty, edit) => {
176
+ if (tableProperty === void 0) {
177
+ return "unknown-table";
178
+ }
179
+ const initializer = tableProperty.getInitializer();
180
+ if (initializer === void 0) {
181
+ return "unknown-table";
182
+ }
183
+ const duplicate = collectCalls(initializer).some((call) => {
184
+ const expression = call.getExpression();
185
+ if (expression.getKind() !== SyntaxKind.PropertyAccessExpression) {
186
+ return false;
187
+ }
188
+ const access = expression.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
189
+ const [nameArgument] = call.getArguments();
190
+ return access.getName() === "index" && nameArgument?.getKind() === SyntaxKind.StringLiteral && nameArgument.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralText() === edit.name;
191
+ });
192
+ if (duplicate) {
193
+ return "duplicate-index";
194
+ }
195
+ const fields = edit.fields.map((field) => JSON.stringify(field)).join(", ");
196
+ const options = edit.unique === true ? ", { unique: true }" : "";
197
+ tableProperty.setInitializer(`${initializer.getText()}.index(${JSON.stringify(edit.name)}, [${fields}]${options})`);
198
+ return void 0;
199
+ };
200
+ const applyAdditiveEdit = (source, edit) => {
201
+ if (classifyEdit(edit) === "destructive") {
202
+ return { ok: false, reason: "destructive" };
203
+ }
204
+ const additive = edit;
205
+ const invalid = validateAdditiveEdit(additive);
206
+ if (invalid !== void 0) {
207
+ return { ok: false, reason: invalid };
208
+ }
209
+ const located = findTablesObject(source);
210
+ if ("reason" in located) {
211
+ return { ok: false, reason: located.reason };
212
+ }
213
+ const { object: tablesObject, sourceFile } = located;
214
+ let failure;
215
+ if (additive.kind === "addTable") {
216
+ failure = applyAddTable(tablesObject, additive);
217
+ } else if (additive.kind === "addOptionalColumn") {
218
+ failure = applyAddOptionalColumn(tablesObject, additive);
219
+ } else {
220
+ failure = applyAddIndex(findTableProperty(tablesObject, additive.table), additive);
221
+ }
222
+ if (failure !== void 0) {
223
+ return { ok: false, reason: failure };
224
+ }
225
+ return { ok: true, text: sourceFile.getFullText() };
226
+ };
227
+
228
+ export { applyAdditiveEdit, classifyEdit };
@@ -0,0 +1,187 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { existsSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, parseDevVariableEntries, DEV_VARS_NEWLINE, splitDevVariableLine, unquoteDevVariable } from './DEV_VARS_EXAMPLE_FILE-dJPNTEnK.mjs';
5
+ import { secretsForPackages } from './PACKAGE_SECRETS_REGISTRY-CySy5vR_.mjs';
6
+
7
+ const SECRET_BYTES = 32;
8
+ const SECRET_KEY = /(?:KEY|PASSWORD|SECRET|TOKEN)$/u;
9
+ const PLACEHOLDER_MARKERS = [
10
+ "replace",
11
+ "openssl",
12
+ "changeme",
13
+ "change-me",
14
+ "change_me",
15
+ "change-this",
16
+ "change_this",
17
+ "your-",
18
+ "your_",
19
+ "example",
20
+ "placeholder",
21
+ "todo",
22
+ "fill-me",
23
+ "fill_me",
24
+ "fill-in",
25
+ "fill_in",
26
+ "xxx"
27
+ ];
28
+ const REGEXP_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/gu;
29
+ const MARKER_ENDS_ALPHANUMERIC = /[a-z0-9]$/u;
30
+ const isPlaceholderValue = (value) => {
31
+ const normalised = value.trim().toLowerCase();
32
+ if (normalised === "") {
33
+ return true;
34
+ }
35
+ if (normalised.startsWith("<") && normalised.endsWith(">")) {
36
+ return true;
37
+ }
38
+ return PLACEHOLDER_MARKERS.some((marker) => {
39
+ const escaped = marker.replaceAll(REGEXP_SPECIAL_CHARS, String.raw`\$&`);
40
+ const needsTrailingBoundary = MARKER_ENDS_ALPHANUMERIC.test(marker);
41
+ const pattern = needsTrailingBoundary ? `(^|[^a-z0-9])${escaped}([^a-z0-9]|$)` : `(^|[^a-z0-9])${escaped}`;
42
+ return new RegExp(pattern, "u").test(normalised);
43
+ });
44
+ };
45
+ const isPlaceholder = (rawValue) => isPlaceholderValue(unquoteDevVariable(rawValue.trim()));
46
+ const defaultRandomHex = (bytes) => randomBytes(bytes).toString("hex");
47
+ const generatedSecretFor = (key, rawValue, randomHex) => SECRET_KEY.test(key) && isPlaceholder(rawValue) ? randomHex(SECRET_BYTES) : void 0;
48
+ const planDevVariablesScaffold = (input) => {
49
+ if (input.devVarsExists) {
50
+ return { status: "exists" };
51
+ }
52
+ if (input.exampleContent === void 0) {
53
+ return { status: "no-example" };
54
+ }
55
+ const randomHex = input.randomHex ?? defaultRandomHex;
56
+ const generatedKeys = [];
57
+ const lines = input.exampleContent.split(DEV_VARS_NEWLINE).map((line) => {
58
+ const parsed = splitDevVariableLine(line);
59
+ const secret = parsed ? generatedSecretFor(parsed.key, parsed.value, randomHex) : void 0;
60
+ if (!parsed || secret === void 0) {
61
+ return line;
62
+ }
63
+ generatedKeys.push(parsed.key);
64
+ return `${parsed.key}="${secret}"`;
65
+ });
66
+ return { content: lines.join("\n"), generatedKeys, status: "generate" };
67
+ };
68
+ const planDevVariablesAugment = (input) => {
69
+ const randomHex = input.randomHex ?? defaultRandomHex;
70
+ const present = new Set(parseDevVariableEntries(input.existingContent).map((entry) => entry.key));
71
+ const additions = [];
72
+ const generatedKeys = [];
73
+ const missingKeys = [];
74
+ for (const line of input.exampleContent.split(DEV_VARS_NEWLINE)) {
75
+ const parsed = splitDevVariableLine(line);
76
+ if (!parsed || present.has(parsed.key)) {
77
+ continue;
78
+ }
79
+ const secret = generatedSecretFor(parsed.key, parsed.value, randomHex);
80
+ missingKeys.push(parsed.key);
81
+ if (secret === void 0) {
82
+ additions.push(`${parsed.key}="${unquoteDevVariable(parsed.value)}"`);
83
+ } else {
84
+ generatedKeys.push(parsed.key);
85
+ additions.push(`${parsed.key}="${secret}"`);
86
+ }
87
+ }
88
+ return { additions, generatedKeys, missingKeys };
89
+ };
90
+ const generatedSuffix = (keys) => keys.length > 0 ? ` (generated ${keys.join(", ")})` : "";
91
+ const atomicCreateDevVariables = (path, content) => {
92
+ const temporaryPath = `${path}.tmp-${String(process.pid)}`;
93
+ try {
94
+ writeFileSync(temporaryPath, content, { encoding: "utf8", flag: "wx", mode: 384 });
95
+ renameSync(temporaryPath, path);
96
+ } catch (error) {
97
+ rmSync(temporaryPath, { force: true });
98
+ throw error;
99
+ }
100
+ };
101
+ const appendDevVariables = (path, additions) => {
102
+ const existing = readFileSync(path, "utf8");
103
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
104
+ const content = `${existing}${separator}${additions.join("\n")}
105
+ `;
106
+ const temporaryPath = `${path}.tmp-${String(process.pid)}`;
107
+ try {
108
+ writeFileSync(temporaryPath, content, { encoding: "utf8", mode: 384 });
109
+ renameSync(temporaryPath, path);
110
+ } catch (error) {
111
+ rmSync(temporaryPath, { force: true });
112
+ throw error;
113
+ }
114
+ };
115
+ const ensureDevVariables = async (deps) => {
116
+ const devVariablesPath = join(deps.cwd, DEV_VARS_FILE);
117
+ const examplePath = join(deps.cwd, DEV_VARS_EXAMPLE_FILE);
118
+ if (!existsSync(examplePath)) {
119
+ return { addedKeys: [], generatedKeys: [], status: "no-example" };
120
+ }
121
+ const exampleContent = readFileSync(examplePath, "utf8");
122
+ if (!existsSync(devVariablesPath)) {
123
+ const plan = planDevVariablesScaffold({ devVarsExists: false, exampleContent, randomHex: deps.randomHex });
124
+ if (plan.status !== "generate") {
125
+ return { addedKeys: [], generatedKeys: [], status: "no-example" };
126
+ }
127
+ const proceed2 = deps.yes === true || await deps.confirm(`No ${DEV_VARS_FILE} found. Generate it from ${DEV_VARS_EXAMPLE_FILE} (secrets auto-filled)?`);
128
+ if (!proceed2) {
129
+ deps.info(`Skipped — copy ${DEV_VARS_EXAMPLE_FILE} to ${DEV_VARS_FILE} and fill it in when you're ready.`);
130
+ return { addedKeys: [], generatedKeys: [], status: "declined" };
131
+ }
132
+ if (existsSync(devVariablesPath)) {
133
+ return { addedKeys: [], generatedKeys: [], status: "skipped-exists" };
134
+ }
135
+ atomicCreateDevVariables(devVariablesPath, plan.content);
136
+ deps.info(`Created ${DEV_VARS_FILE}${generatedSuffix(plan.generatedKeys)}.`);
137
+ return { addedKeys: [], generatedKeys: plan.generatedKeys, status: "generated" };
138
+ }
139
+ const augment = planDevVariablesAugment({ existingContent: readFileSync(devVariablesPath, "utf8"), exampleContent, randomHex: deps.randomHex });
140
+ if (augment.missingKeys.length === 0) {
141
+ return { addedKeys: [], generatedKeys: [], status: "exists" };
142
+ }
143
+ const list = augment.missingKeys.join(", ");
144
+ const proceed = deps.yes === true || await deps.confirm(`${DEV_VARS_FILE} is missing ${String(augment.missingKeys.length)} key(s) from ${DEV_VARS_EXAMPLE_FILE} (${list}). Add them?`);
145
+ if (!proceed) {
146
+ deps.info(`Skipped — add ${list} to ${DEV_VARS_FILE} when you're ready.`);
147
+ return { addedKeys: [], generatedKeys: [], status: "declined" };
148
+ }
149
+ appendDevVariables(devVariablesPath, augment.additions);
150
+ deps.info(`Updated ${DEV_VARS_FILE} — added ${list}${generatedSuffix(augment.generatedKeys)}.`);
151
+ return { addedKeys: augment.missingKeys, generatedKeys: augment.generatedKeys, status: "augmented" };
152
+ };
153
+ const secretEntryBlock = (entry) => {
154
+ const lines = [`# ${entry.description}`, `# Docs: ${entry.docsUrl}`, `${entry.key}="${entry.placeholderValue}"`];
155
+ return lines.join("\n");
156
+ };
157
+ const buildPackageSecretsBlock = (packageNames, existingKeys) => {
158
+ const entries = secretsForPackages(packageNames).filter((entry) => !existingKeys.has(entry.key));
159
+ if (entries.length === 0) {
160
+ return "";
161
+ }
162
+ return entries.map((entry) => secretEntryBlock(entry)).join("\n\n");
163
+ };
164
+ const ensureDevVariablesExample = (cwd, packageNames) => {
165
+ const examplePath = join(cwd, DEV_VARS_EXAMPLE_FILE);
166
+ const existing = existsSync(examplePath) ? readFileSync(examplePath, "utf8") : "";
167
+ const existingKeys = new Set(parseDevVariableEntries(existing).map((entry) => entry.key));
168
+ const block = buildPackageSecretsBlock(packageNames, existingKeys);
169
+ if (block === "") {
170
+ return [];
171
+ }
172
+ const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
173
+ const newContent = `${existing}${separator}
174
+ ${block}
175
+ `;
176
+ const temporaryPath = `${examplePath}.tmp-${String(process.pid)}`;
177
+ try {
178
+ writeFileSync(temporaryPath, newContent, { encoding: "utf8" });
179
+ renameSync(temporaryPath, examplePath);
180
+ } catch (error) {
181
+ rmSync(temporaryPath, { force: true });
182
+ throw error;
183
+ }
184
+ return secretsForPackages(packageNames).filter((entry) => !existingKeys.has(entry.key)).map((entry) => entry.key);
185
+ };
186
+
187
+ export { buildPackageSecretsBlock, ensureDevVariables, ensureDevVariablesExample as ensureDevVarsExample, isPlaceholderValue, planDevVariablesAugment, planDevVariablesScaffold };
@@ -0,0 +1,99 @@
1
+ import { Project, SyntaxKind } from 'ts-morph';
2
+
3
+ const TERMINAL_METHODS = /* @__PURE__ */ new Set(["action", "mutation", "query", "stream"]);
4
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/u;
5
+ const ADDITIVE_KINDS = /* @__PURE__ */ new Set(["scaffoldPolicy", "wireRls"]);
6
+ const classifyPolicyEdit = (edit) => ADDITIVE_KINDS.has(edit.kind) ? "additive" : "destructive";
7
+ const scaffoldPolicyFile = (edit) => {
8
+ if (typeof edit.name !== "string" || typeof edit.table !== "string" || !IDENTIFIER_PATTERN.test(edit.name) || !IDENTIFIER_PATTERN.test(edit.table)) {
9
+ return { ok: false, reason: "invalid-identifier" };
10
+ }
11
+ const policiesIdentifier = `${edit.name}Policies`;
12
+ const rolesIdentifier = `${edit.name}Roles`;
13
+ const table = JSON.stringify(edit.table);
14
+ const source = `import { definePermission, definePolicies, definePolicy, defineRole } from "@lunora/server";
15
+
16
+ /**
17
+ * Access rules for the ${edit.table} table — scaffolded by the Lunora studio
18
+ * (plan 025). The \`when\` predicates below DENY by default (\`() => false\`);
19
+ * replace each TODO with the real condition, then wire \`${policiesIdentifier}\`
20
+ * into the procedures that read/write ${edit.table} via
21
+ * \`.use(rls(${policiesIdentifier}, { roles: ${rolesIdentifier} }))\`.
22
+ */
23
+ export const ${policiesIdentifier} = definePolicies([
24
+ definePolicy({
25
+ on: "read",
26
+ table: ${table},
27
+ // TODO: return \`true\` to allow, a \`WhereInput\` to filter, or \`false\` to deny.
28
+ when: () => false,
29
+ }),
30
+ ]);
31
+
32
+ /** Named permissions \`${policiesIdentifier}\` can check with \`ctx.auth.can(...)\`. */
33
+ export const ${edit.name}View = definePermission("${edit.name}:view");
34
+
35
+ /** Roles that grant the permissions above; register via \`rls(policies, { roles })\`. */
36
+ export const ${rolesIdentifier} = [defineRole("${edit.name}-admin", { permissions: [${edit.name}View] })];
37
+ `;
38
+ return { fileName: `${edit.name}.policies.ts`, ok: true, source };
39
+ };
40
+ const builderReceiver = (initializer) => {
41
+ const callee = initializer.getExpression();
42
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression) {
43
+ return void 0;
44
+ }
45
+ const access = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
46
+ return TERMINAL_METHODS.has(access.getName()) ? access.getExpression() : void 0;
47
+ };
48
+ const ensureRlsImport = (sourceFile) => {
49
+ const serverImport = sourceFile.getImportDeclaration((declaration) => declaration.getModuleSpecifierValue() === "@lunora/server");
50
+ if (serverImport === void 0) {
51
+ sourceFile.addImportDeclaration({ moduleSpecifier: "@lunora/server", namedImports: ["rls"] });
52
+ return;
53
+ }
54
+ if (!serverImport.getNamedImports().some((named) => named.getName() === "rls")) {
55
+ serverImport.addNamedImport("rls");
56
+ }
57
+ };
58
+ const chainHasRls = (receiver) => {
59
+ const calls = receiver.getDescendantsOfKind(SyntaxKind.CallExpression);
60
+ if (receiver.getKind() === SyntaxKind.CallExpression) {
61
+ calls.push(receiver.asKindOrThrow(SyntaxKind.CallExpression));
62
+ }
63
+ for (const call of calls) {
64
+ const callee = call.getExpression();
65
+ if (callee.getKind() !== SyntaxKind.PropertyAccessExpression) {
66
+ continue;
67
+ }
68
+ const access = callee.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
69
+ const [argument] = call.getArguments();
70
+ if (access.getName() === "use" && argument?.getKind() === SyntaxKind.CallExpression && argument.asKindOrThrow(SyntaxKind.CallExpression).getExpression().getText() === "rls") {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ };
76
+ const wireRlsIntoProcedure = (source, edit) => {
77
+ if (typeof edit.policies !== "string" || !IDENTIFIER_PATTERN.test(edit.policies)) {
78
+ return { ok: false, reason: "invalid-identifier" };
79
+ }
80
+ const project = new Project({ compilerOptions: { allowJs: true }, useInMemoryFileSystem: true });
81
+ const sourceFile = project.createSourceFile("procedure.ts", source, { overwrite: true });
82
+ const declaration = sourceFile.getVariableDeclaration(edit.exportName);
83
+ const initializer = declaration?.getInitializer();
84
+ if (declaration === void 0 || initializer?.getKind() !== SyntaxKind.CallExpression) {
85
+ return { ok: false, reason: "unknown-procedure" };
86
+ }
87
+ const receiver = builderReceiver(initializer.asKindOrThrow(SyntaxKind.CallExpression));
88
+ if (receiver === void 0) {
89
+ return { ok: false, reason: "unsupported-procedure-shape" };
90
+ }
91
+ if (chainHasRls(receiver)) {
92
+ return { ok: false, reason: "already-wired" };
93
+ }
94
+ receiver.replaceWithText(`${receiver.getText()}.use(rls(${edit.policies}))`);
95
+ ensureRlsImport(sourceFile);
96
+ return { ok: true, text: sourceFile.getFullText() };
97
+ };
98
+
99
+ export { classifyPolicyEdit, scaffoldPolicyFile, wireRlsIntoProcedure };
@@ -0,0 +1,100 @@
1
+ import { createInterface } from 'node:readline';
2
+
3
+ const MULTI_SELECT_SEPARATOR = /[\s,]+/u;
4
+ const isInteractive = () => process.stdin.isTTY;
5
+ const promptYesNo = async (prompt, options) => {
6
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
7
+ try {
8
+ const answer = await new Promise((resolve) => {
9
+ rl.question(prompt, (input) => {
10
+ resolve(input);
11
+ });
12
+ });
13
+ const normalised = answer.trim().toLowerCase();
14
+ if (normalised === "") {
15
+ return options?.defaultYes === true;
16
+ }
17
+ return normalised === "y" || normalised === "yes";
18
+ } finally {
19
+ rl.close();
20
+ }
21
+ };
22
+ const createConfirm = (prefix = "") => isInteractive() ? (message) => promptYesNo(`${prefix}${message} [Y/n] `, { defaultYes: true }) : () => Promise.resolve(false);
23
+ const promptSelect = async (message, options, settings) => {
24
+ if (!isInteractive() || options.length === 0) {
25
+ return settings?.default;
26
+ }
27
+ const defaultIndex = settings?.default === void 0 ? -1 : options.findIndex((option) => option.value === settings.default);
28
+ const lines = options.map(
29
+ (option, index) => ` ${String(index + 1)}) ${option.label}${option.description === void 0 ? "" : ` — ${option.description}`}`
30
+ );
31
+ const promptText = `${message}
32
+ ${lines.join("\n")}
33
+ > ${defaultIndex >= 0 ? `[${String(defaultIndex + 1)}] ` : ""}`;
34
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
35
+ try {
36
+ const answer = await new Promise((resolve) => {
37
+ rl.question(promptText, (input) => {
38
+ resolve(input);
39
+ });
40
+ });
41
+ const trimmed = answer.trim();
42
+ if (trimmed === "") {
43
+ return settings?.default;
44
+ }
45
+ const choice = Number.parseInt(trimmed, 10);
46
+ if (Number.isInteger(choice) && choice >= 1 && choice <= options.length) {
47
+ return options[choice - 1]?.value;
48
+ }
49
+ const byText = options.find((option) => option.value === trimmed || option.label.toLowerCase() === trimmed.toLowerCase());
50
+ return byText?.value ?? settings?.default;
51
+ } finally {
52
+ rl.close();
53
+ }
54
+ };
55
+ const promptMultiSelect = async (message, options, settings) => {
56
+ const defaults = settings?.defaults ?? [];
57
+ if (!isInteractive() || options.length === 0) {
58
+ return [...defaults];
59
+ }
60
+ const lines = options.map(
61
+ (option, index) => ` ${String(index + 1)}) ${option.label}${option.description === void 0 ? "" : ` — ${option.description}`}`
62
+ );
63
+ const defaultHint = defaults.length === 0 ? "" : `[${defaults.join(", ")}] `;
64
+ const promptText = `${message} (comma-separated; Enter for default)
65
+ ${lines.join("\n")}
66
+ > ${defaultHint}`;
67
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
68
+ try {
69
+ const answer = await new Promise((resolve) => {
70
+ rl.question(promptText, (input) => {
71
+ resolve(input);
72
+ });
73
+ });
74
+ const trimmed = answer.trim();
75
+ if (trimmed === "") {
76
+ return [...defaults];
77
+ }
78
+ const tokens = trimmed.split(MULTI_SELECT_SEPARATOR).map((token) => token.trim()).filter((token) => token !== "");
79
+ const picked = /* @__PURE__ */ new Set();
80
+ for (const token of tokens) {
81
+ const choice = Number.parseInt(token, 10);
82
+ if (Number.isInteger(choice) && choice >= 1 && choice <= options.length && String(choice) === token) {
83
+ const byIndex = options[choice - 1]?.value;
84
+ if (byIndex !== void 0) {
85
+ picked.add(byIndex);
86
+ }
87
+ continue;
88
+ }
89
+ const byText = options.find((option) => option.value === token || option.label.toLowerCase() === token.toLowerCase());
90
+ if (byText !== void 0) {
91
+ picked.add(byText.value);
92
+ }
93
+ }
94
+ return options.filter((option) => picked.has(option.value)).map((option) => option.value);
95
+ } finally {
96
+ rl.close();
97
+ }
98
+ };
99
+
100
+ export { createConfirm, isInteractive, promptMultiSelect, promptSelect, promptYesNo };
@@ -0,0 +1,41 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const FRAMEWORK_SIGNATURES = [
5
+ { class: "A", dependency: "@tanstack/react-start", framework: "tanstack-start" },
6
+ { class: "A", dependency: "@tanstack/solid-start", framework: "tanstack-start-solid" },
7
+ { class: "A", dependency: "@react-router/dev", framework: "react-router" },
8
+ { class: "A", dependency: "@solidjs/start", framework: "solid-start" },
9
+ { class: "A", dependency: "solid-start", framework: "solid-start" },
10
+ { class: "B", dependency: "@sveltejs/kit", framework: "sveltekit" },
11
+ { class: "B", dependency: "nuxt", framework: "nuxt" },
12
+ { class: "B", dependency: "astro", framework: "astro" }
13
+ ];
14
+ const STANDALONE = { class: "C", framework: "none" };
15
+ const readDependencyNames = (root) => {
16
+ const packageJsonPath = join(root, "package.json");
17
+ if (!existsSync(packageJsonPath)) {
18
+ return /* @__PURE__ */ new Set();
19
+ }
20
+ try {
21
+ const raw = readFileSync(packageJsonPath, "utf8");
22
+ const parsed = JSON.parse(raw);
23
+ return /* @__PURE__ */ new Set([...Object.keys(parsed.dependencies ?? {}), ...Object.keys(parsed.devDependencies ?? {})]);
24
+ } catch {
25
+ return /* @__PURE__ */ new Set();
26
+ }
27
+ };
28
+ const detectFramework = (root) => {
29
+ const dependencies = readDependencyNames(root);
30
+ if (dependencies.size === 0) {
31
+ return STANDALONE;
32
+ }
33
+ for (const signature of FRAMEWORK_SIGNATURES) {
34
+ if (dependencies.has(signature.dependency)) {
35
+ return { class: signature.class, framework: signature.framework };
36
+ }
37
+ }
38
+ return STANDALONE;
39
+ };
40
+
41
+ export { detectFramework };
@@ -0,0 +1,19 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { CONTAINERS_FILENAME, discoverContainers } from '@lunora/codegen';
3
+ import { Project } from 'ts-morph';
4
+ import { join } from 'node:path';
5
+
6
+ const discoverContainerInfo = (projectRoot, schemaDirectory) => {
7
+ const containersPath = join(projectRoot, schemaDirectory, CONTAINERS_FILENAME);
8
+ if (!existsSync(containersPath)) {
9
+ return { containers: [] };
10
+ }
11
+ try {
12
+ const project = new Project({ skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: true, useInMemoryFileSystem: false });
13
+ return { containers: discoverContainers(project, join(projectRoot, schemaDirectory)) };
14
+ } catch (error) {
15
+ return { containers: [], error: error instanceof Error ? error.message : String(error) };
16
+ }
17
+ };
18
+
19
+ export { discoverContainerInfo };