@arkstack/console 0.5.2 → 0.5.3

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/README.md CHANGED
@@ -1,3 +1,5 @@
1
1
  # @arkstack/console
2
2
 
3
+ [![@arkstack/console](https://img.shields.io/npm/dt/@arkstack/console?style=flat-square&label=@arkstack/console&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F@arkstack/console)](https://www.npmjs.com/package/@arkstack/console)
4
+
3
5
  Console module for Arkstack, providing the command-line runtime and console integration layer.
@@ -0,0 +1,288 @@
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { Arkstack } from "@arkstack/contract";
4
+ import { Node, Project } from "ts-morph";
5
+
6
+ //#region src/prepare/TSConfig.ts
7
+ const TSConfig = {
8
+ compilerOptions: {
9
+ module: "es2022",
10
+ target: "es2022",
11
+ lib: ["es2022", "DOM"],
12
+ moduleResolution: "bundler",
13
+ esModuleInterop: true,
14
+ forceConsistentCasingInFileNames: true,
15
+ strict: true,
16
+ skipLibCheck: true,
17
+ skipDefaultLibCheck: true,
18
+ strictFunctionTypes: true,
19
+ strictPropertyInitialization: true,
20
+ strictBindCallApply: true,
21
+ noImplicitAny: true,
22
+ removeComments: true,
23
+ experimentalDecorators: true,
24
+ emitDecoratorMetadata: true,
25
+ paths: {
26
+ "@": ["../src"],
27
+ "src/*": ["../src/*"],
28
+ "@app/*": ["../src/app/*"],
29
+ "@core/*": ["../src/core/*"],
30
+ "@controllers/*": ["../src/app/http/controllers/*"],
31
+ "@models/*": ["../src/app/models/*"]
32
+ }
33
+ },
34
+ include: [
35
+ "./*.d.ts",
36
+ "../src",
37
+ "../tests"
38
+ ]
39
+ };
40
+ const BaseTCConfig = { extends: "./.arkstack/tsconfig.json" };
41
+
42
+ //#endregion
43
+ //#region src/prepare/BuildInterfaces.ts
44
+ var BuildInterfaces = class BuildInterfaces {
45
+ static project;
46
+ static checker;
47
+ /**
48
+ * Generate configuration interfaces
49
+ *
50
+ * @param configDir
51
+ */
52
+ static configs(configDir) {
53
+ configDir ??= path.join(Arkstack.rootDir(), "src/config");
54
+ const declaration = this.generateConfig(configDir);
55
+ writeFileSync(path.join(Arkstack.rootDir(), ".arkstack/ark.d.ts"), declaration, "utf8");
56
+ }
57
+ static tsconfig() {
58
+ const configs = {
59
+ ".arkstack/tsconfig.json": JSON.stringify(TSConfig, void 0, 2),
60
+ "tsconfig.json": JSON.stringify(BaseTCConfig, void 0, 2)
61
+ };
62
+ for (const [file, config] of Object.entries(configs)) writeFileSync(path.join(Arkstack.rootDir(), file), config, "utf8");
63
+ }
64
+ /**
65
+ * Generate an `EnvRegistry` augmentation from the application's `.env`
66
+ * schema, giving `env()` precise return types for app-specific variables.
67
+ *
68
+ * Mirrors {@link configs}: the declaration is appended to
69
+ * `.arkstack/ark.d.ts`. Keys already typed by the framework's own
70
+ * `EnvRegistry` are skipped so declaration merging never conflicts.
71
+ *
72
+ * @param envFile Explicit `.env` path; defaults to `.env.example` then `.env`.
73
+ */
74
+ static env(envFile) {
75
+ const root = Arkstack.rootDir();
76
+ const file = envFile ?? BuildInterfaces.resolveEnvFile(root);
77
+ if (!file) return;
78
+ const declaration = BuildInterfaces.envRegistryFromEnv(readFileSync(file, "utf8"), [...BuildInterfaces.frameworkEnvKeys()]);
79
+ if (!declaration) return;
80
+ const target = path.join(root, ".arkstack/ark.d.ts");
81
+ let content = `${existsSync(target) ? readFileSync(target, "utf8") : ""}\n${declaration}\n`;
82
+ if (!/^\s*export\s|\bimport\s/m.test(content)) content += "\nexport {}\n";
83
+ writeFileSync(target, content, "utf8");
84
+ }
85
+ /**
86
+ * Render an `EnvRegistry` augmentation for the variables declared in the
87
+ * given `.env` contents. Pure (no filesystem) for testability.
88
+ *
89
+ * @param contents Raw `.env` file contents.
90
+ * @param skip Variable names to omit (e.g. framework-owned keys).
91
+ * @returns The `declare module` block, or `''` when nothing to emit.
92
+ */
93
+ static envRegistryFromEnv(contents, skip = []) {
94
+ const skipped = new Set(skip);
95
+ const properties = Object.entries(BuildInterfaces.parseEnvFile(contents)).filter(([key]) => !skipped.has(key)).map(([key, value]) => ` ${key}: ${BuildInterfaces.inferEnvType(value)}`);
96
+ if (!properties.length) return "";
97
+ return [
98
+ "declare module '@arkstack/common' {",
99
+ " interface EnvRegistry {",
100
+ ...properties,
101
+ " }",
102
+ "}"
103
+ ].join("\n");
104
+ }
105
+ /**
106
+ * Prefer `.env.example` (the documented schema), then `.env`.
107
+ *
108
+ * @param root
109
+ * @returns
110
+ */
111
+ static resolveEnvFile(root) {
112
+ for (const name of [".env.example", ".env"]) {
113
+ const file = path.join(root, name);
114
+ if (existsSync(file)) return file;
115
+ }
116
+ }
117
+ /**
118
+ * Parse `KEY=VALUE` lines, skipping blanks, comments and invalid names.
119
+ *
120
+ * @param contents
121
+ * @returns
122
+ */
123
+ static parseEnvFile(contents) {
124
+ const entries = {};
125
+ for (const raw of contents.split(/\r?\n/)) {
126
+ const line = raw.trim();
127
+ if (!line || line.startsWith("#")) continue;
128
+ const eq = line.indexOf("=");
129
+ if (eq === -1) continue;
130
+ const key = line.slice(0, eq).trim();
131
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
132
+ let value = line.slice(eq + 1).trim();
133
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
134
+ entries[key] = value;
135
+ }
136
+ return entries;
137
+ }
138
+ /**
139
+ * Infer a TS type from a value, mirroring env()'s runtime coercion.
140
+ *
141
+ * @param value
142
+ * @returns
143
+ */
144
+ static inferEnvType(value) {
145
+ if (value === "") return "string";
146
+ if ([
147
+ "true",
148
+ "false",
149
+ "on",
150
+ "off"
151
+ ].includes(value)) return "boolean";
152
+ if (!Number.isNaN(Number(value))) return "number";
153
+ return "string";
154
+ }
155
+ /**
156
+ * Resolve the framework's own `EnvRegistry` keys to skip during generation.
157
+ *
158
+ * @returns
159
+ */
160
+ static frameworkEnvKeys() {
161
+ try {
162
+ const keys = new Project({
163
+ tsConfigFilePath: path.join(Arkstack.rootDir(), "tsconfig.json"),
164
+ skipAddingFilesFromTsConfig: true
165
+ }).createSourceFile("__ark_env_probe__.ts", "import type { EnvRegistry } from '@arkstack/common'\ndeclare const value: EnvRegistry\n", { overwrite: true }).getVariableDeclarationOrThrow("value").getType().getProperties().map((prop) => prop.getName());
166
+ return new Set(keys);
167
+ } catch {
168
+ return /* @__PURE__ */ new Set();
169
+ }
170
+ }
171
+ static generateConfig(configDir = path.join(process.cwd(), "src/config")) {
172
+ BuildInterfaces.project = new Project({
173
+ tsConfigFilePath: path.join(process.cwd(), "tsconfig.json"),
174
+ skipAddingFilesFromTsConfig: true
175
+ });
176
+ BuildInterfaces.checker = BuildInterfaces.project.getTypeChecker();
177
+ const files = readdirSync(configDir).filter((f) => f.endsWith(".ts"));
178
+ const imports = /* @__PURE__ */ new Map();
179
+ const properties = [];
180
+ for (const file of files) {
181
+ const configName = path.basename(file, ".ts");
182
+ const sourceFile = BuildInterfaces.project.addSourceFileAtPath(path.join(configDir, file));
183
+ const exportAssignment = sourceFile.getExportAssignment((e) => !e.isExportEquals());
184
+ if (!exportAssignment) continue;
185
+ const expr = exportAssignment.getExpression();
186
+ const annotatedType = BuildInterfaces.resolveReturnTypeAnnotation(sourceFile, expr, imports);
187
+ if (annotatedType) {
188
+ properties.push(` ${configName}: ${annotatedType}`);
189
+ continue;
190
+ }
191
+ const type = BuildInterfaces.checker.getTypeAtLocation(expr);
192
+ const callSignatures = type.getCallSignatures();
193
+ const resolvedType = callSignatures.length ? callSignatures[0].getReturnType() : type;
194
+ const typeStr = BuildInterfaces.resolveType(resolvedType, 2);
195
+ properties.push(` ${configName}: ${typeStr}`);
196
+ }
197
+ return [
198
+ ...BuildInterfaces.renderImports(imports),
199
+ "declare module '@arkstack/common' {",
200
+ " interface ConfigRegistry {",
201
+ ...properties,
202
+ " }",
203
+ "}",
204
+ "",
205
+ "export {}"
206
+ ].join("\n");
207
+ }
208
+ static resolveReturnTypeAnnotation(sourceFile, expr, imports) {
209
+ if (!Node.isArrowFunction(expr) && !Node.isFunctionExpression(expr)) return;
210
+ const returnType = expr.getReturnTypeNode();
211
+ if (!returnType) return;
212
+ const referencedNames = /* @__PURE__ */ new Set();
213
+ if (Node.isTypeReference(returnType)) referencedNames.add(returnType.getTypeName().getText());
214
+ returnType.forEachDescendant((node) => {
215
+ if (Node.isTypeReference(node)) referencedNames.add(node.getTypeName().getText());
216
+ });
217
+ for (const name of referencedNames) {
218
+ const imported = BuildInterfaces.findNamedTypeImport(sourceFile, name);
219
+ if (imported) {
220
+ const specifiers = imports.get(imported.moduleSpecifier) ?? /* @__PURE__ */ new Set();
221
+ specifiers.add(imported.specifier);
222
+ imports.set(imported.moduleSpecifier, specifiers);
223
+ }
224
+ }
225
+ return returnType.getText(!!sourceFile);
226
+ }
227
+ static findNamedTypeImport(sourceFile, name) {
228
+ for (const declaration of sourceFile.getImportDeclarations()) for (const namedImport of declaration.getNamedImports()) {
229
+ const alias = namedImport.getAliasNode()?.getText();
230
+ const importedName = namedImport.getName();
231
+ if ((alias ?? importedName) !== name) continue;
232
+ return {
233
+ moduleSpecifier: declaration.getModuleSpecifierValue(),
234
+ specifier: alias ? `${importedName} as ${alias}` : importedName
235
+ };
236
+ }
237
+ }
238
+ static renderImports(imports) {
239
+ return [...imports.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([moduleSpecifier, specifiers]) => {
240
+ return `import type { ${[...specifiers].sort().join(", ")} } from '${moduleSpecifier}'`;
241
+ });
242
+ }
243
+ /**
244
+ * ts-morph resolves computed keys like path.join(...) as { [x: number]: any }
245
+ * detect these by checking the type text for an index signature pattern
246
+ *
247
+ * @param type
248
+ * @returns
249
+ */
250
+ static isDynamicMap(type) {
251
+ return /^\{ \[x: (string|number)\]:/.test(type.getText());
252
+ }
253
+ static resolveType(type, indent) {
254
+ const pad = " ".repeat(indent);
255
+ const innerPad = " ".repeat(indent + 1);
256
+ if (type.isString()) return "string";
257
+ if (type.isNumber()) return "number";
258
+ if (type.isBoolean()) return "boolean";
259
+ if (type.isNull()) return "null";
260
+ if (type.isUndefined()) return "undefined";
261
+ if (type.isAny() || type.isUnknown()) return "any";
262
+ if (type.isStringLiteral()) return `'${type.getLiteralValue()}'`;
263
+ if (type.isNumberLiteral()) return String(type.getLiteralValue());
264
+ if (type.isBooleanLiteral()) return String(type.getLiteralValue());
265
+ if (type.isUnion()) return type.getUnionTypes().map((t) => BuildInterfaces.resolveType(t, indent)).join(" | ");
266
+ if (type.isArray()) {
267
+ const elementType = type.getArrayElementTypeOrThrow();
268
+ return `${BuildInterfaces.resolveType(elementType, indent)}[]`;
269
+ }
270
+ if (type.getCallSignatures().length) return "Function";
271
+ if (type.isObject()) {
272
+ if (BuildInterfaces.isDynamicMap(type)) return "Record<string, string>";
273
+ const props = type.getProperties();
274
+ if (!props.length) return "Record<string, any>";
275
+ return `{\n${props.map((prop) => {
276
+ const decl = prop.getDeclarations()[0];
277
+ if (!decl) return `${innerPad}${prop.getName()}: any`;
278
+ const propType = BuildInterfaces.checker.getTypeOfSymbolAtLocation(prop, decl);
279
+ const optional = prop.isOptional() ? "?" : "";
280
+ return `${innerPad}${prop.getName()}${optional}: ${BuildInterfaces.resolveType(propType, indent + 1)}`;
281
+ }).join("\n")}\n${pad}}`;
282
+ }
283
+ return type.getText();
284
+ }
285
+ };
286
+
287
+ //#endregion
288
+ export { BaseTCConfig as n, TSConfig as r, BuildInterfaces as t };
@@ -1,6 +1,7 @@
1
+ import { existsSync } from "node:fs";
1
2
  import path, { isAbsolute, join } from "node:path";
3
+ import { Arkstack } from "@arkstack/contract";
2
4
  import { CliApp } from "resora";
3
- import { existsSync } from "node:fs";
4
5
 
5
6
  //#region dist/config.js
6
7
  const defaultConfig = (app) => {
@@ -22,11 +23,11 @@ const defaultConfig = (app) => {
22
23
  //#region dist/app.js
23
24
  const resolveStubsDir = (config, options, core) => {
24
25
  const configuredDir = config?.localStubsDir;
25
- if (configuredDir) return isAbsolute(configuredDir) ? configuredDir : join(process.cwd(), configuredDir);
26
+ if (configuredDir) return isAbsolute(configuredDir) ? configuredDir : join(Arkstack.rootDir(), configuredDir);
26
27
  if (!options?.stubsDir) {
27
28
  const driver = core?.getDriver().name ?? "h3";
28
- let stubsDir = path.resolve(process.cwd(), `node_modules/@arkstack/driver-${driver}/stubs`);
29
- if (!existsSync(stubsDir)) stubsDir = path.resolve(process.cwd(), "stubs");
29
+ let stubsDir = path.resolve(Arkstack.rootDir(), `node_modules/@arkstack/driver-${driver}/stubs`);
30
+ if (!existsSync(stubsDir)) stubsDir = path.resolve(Arkstack.rootDir(), "stubs");
30
31
  return stubsDir;
31
32
  }
32
33
  return options?.stubsDir;
@@ -44,7 +45,7 @@ var ArkstackConsoleApp = class extends CliApp {
44
45
  this.mergeConfig();
45
46
  const normalized = name.endsWith("Controller") ? name.replace(/controller/i, "") : name;
46
47
  let controllerName = normalized.endsWith("Controller") ? normalized : `${normalized}Controller`;
47
- const outputPath = join(path.resolve(process.cwd(), "src", "app/http/controllers"), `${controllerName}.${opts?.ext ?? "ts"}`);
48
+ const outputPath = join(path.resolve(Arkstack.rootDir(), "src", "app/http/controllers"), `${controllerName}.${opts?.ext ?? "ts"}`);
48
49
  const stubsDir = resolveStubsDir(this.config, this.options, this.core);
49
50
  if (!stubsDir) {
50
51
  console.error("Error: stubsDir is not configured. Set stubsDir in resora.config.js.");
@@ -72,7 +73,7 @@ var ArkstackConsoleApp = class extends CliApp {
72
73
  * @returns
73
74
  */
74
75
  normalizePath = (p) => {
75
- return p.replace(process.cwd(), "");
76
+ return p.replace(Arkstack.rootDir(), "");
76
77
  };
77
78
  /**
78
79
  * Recursively merge defaultConfig with config from resora.config.js, giving
package/dist/app.d.ts CHANGED
@@ -21,7 +21,17 @@ declare class ArkstackConsoleApp<TCore extends Core> extends CliApp {
21
21
  private readonly options;
22
22
  constructor(core: TCore, options: ConsoleAppOptions);
23
23
  makeController: (name: string, opts: any) => string;
24
+ /**
25
+ * Normalize a file path by removing the current working directory from it.
26
+ *
27
+ * @param p
28
+ * @returns
29
+ */
24
30
  normalizePath: (p: string) => string;
31
+ /**
32
+ * Recursively merge defaultConfig with config from resora.config.js, giving
33
+ * precedence to resora.
34
+ */
25
35
  mergeConfig: () => void;
26
36
  }
27
37
  //#endregion
package/dist/app.js CHANGED
@@ -1,3 +1,3 @@
1
- import { n as resolveStubsDir, t as ArkstackConsoleApp } from "./app-CNmh_S9c.js";
1
+ import { n as resolveStubsDir, t as ArkstackConsoleApp } from "./app-DnaQWE9q.js";
2
2
 
3
3
  export { ArkstackConsoleApp, resolveStubsDir };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,119 @@
1
+ //#region src/prepare/BuildInterfaces.d.ts
2
+ declare class BuildInterfaces {
3
+ private static project;
4
+ private static checker;
5
+ /**
6
+ * Generate configuration interfaces
7
+ *
8
+ * @param configDir
9
+ */
10
+ static configs(configDir?: string): void;
11
+ static tsconfig(): void;
12
+ /**
13
+ * Generate an `EnvRegistry` augmentation from the application's `.env`
14
+ * schema, giving `env()` precise return types for app-specific variables.
15
+ *
16
+ * Mirrors {@link configs}: the declaration is appended to
17
+ * `.arkstack/ark.d.ts`. Keys already typed by the framework's own
18
+ * `EnvRegistry` are skipped so declaration merging never conflicts.
19
+ *
20
+ * @param envFile Explicit `.env` path; defaults to `.env.example` then `.env`.
21
+ */
22
+ static env(envFile?: string): void;
23
+ /**
24
+ * Render an `EnvRegistry` augmentation for the variables declared in the
25
+ * given `.env` contents. Pure (no filesystem) for testability.
26
+ *
27
+ * @param contents Raw `.env` file contents.
28
+ * @param skip Variable names to omit (e.g. framework-owned keys).
29
+ * @returns The `declare module` block, or `''` when nothing to emit.
30
+ */
31
+ static envRegistryFromEnv(contents: string, skip?: string[]): string;
32
+ /**
33
+ * Prefer `.env.example` (the documented schema), then `.env`.
34
+ *
35
+ * @param root
36
+ * @returns
37
+ */
38
+ private static resolveEnvFile;
39
+ /**
40
+ * Parse `KEY=VALUE` lines, skipping blanks, comments and invalid names.
41
+ *
42
+ * @param contents
43
+ * @returns
44
+ */
45
+ private static parseEnvFile;
46
+ /**
47
+ * Infer a TS type from a value, mirroring env()'s runtime coercion.
48
+ *
49
+ * @param value
50
+ * @returns
51
+ */
52
+ private static inferEnvType;
53
+ /**
54
+ * Resolve the framework's own `EnvRegistry` keys to skip during generation.
55
+ *
56
+ * @returns
57
+ */
58
+ private static frameworkEnvKeys;
59
+ private static generateConfig;
60
+ private static resolveReturnTypeAnnotation;
61
+ private static findNamedTypeImport;
62
+ private static renderImports;
63
+ /**
64
+ * ts-morph resolves computed keys like path.join(...) as { [x: number]: any }
65
+ * detect these by checking the type text for an index signature pattern
66
+ *
67
+ * @param type
68
+ * @returns
69
+ */
70
+ private static isDynamicMap;
71
+ private static resolveType;
72
+ }
73
+ //#endregion
74
+ //#region src/prepare/TSConfig.d.ts
75
+ declare const TSConfig: {
76
+ compilerOptions: {
77
+ module: string;
78
+ target: string;
79
+ lib: string[];
80
+ moduleResolution: string;
81
+ esModuleInterop: boolean;
82
+ forceConsistentCasingInFileNames: boolean;
83
+ strict: boolean;
84
+ skipLibCheck: boolean;
85
+ skipDefaultLibCheck: boolean;
86
+ strictFunctionTypes: boolean;
87
+ strictPropertyInitialization: boolean;
88
+ strictBindCallApply: boolean;
89
+ noImplicitAny: boolean;
90
+ removeComments: boolean;
91
+ experimentalDecorators: boolean;
92
+ emitDecoratorMetadata: boolean;
93
+ paths: {
94
+ '@': string[];
95
+ 'src/*': string[];
96
+ '@app/*': string[];
97
+ '@core/*': string[];
98
+ '@controllers/*': string[];
99
+ '@models/*': string[];
100
+ };
101
+ };
102
+ include: string[];
103
+ };
104
+ declare const BaseTCConfig: {
105
+ extends: string;
106
+ };
107
+ //#endregion
1
108
  //#region src/index.d.ts
2
109
  interface RunConsoleOptions {
3
110
  logo?: string;
4
111
  }
112
+ /**
113
+ * Runs the console kernel, initializing the application and registering commands.
114
+ *
115
+ * @param options
116
+ */
5
117
  declare const runConsoleKernel: (options?: RunConsoleOptions) => Promise<void>;
6
118
  //#endregion
7
- export { RunConsoleOptions, runConsoleKernel };
119
+ export { BaseTCConfig, BuildInterfaces, RunConsoleOptions, TSConfig, runConsoleKernel };
package/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { t as ArkstackConsoleApp } from "./app-CNmh_S9c.js";
3
- import { config, env, importFile, loadPrototypes, outputDir } from "@arkstack/common";
2
+ import { t as ArkstackConsoleApp } from "./app-DnaQWE9q.js";
3
+ import { n as BaseTCConfig, r as TSConfig, t as BuildInterfaces } from "./BuildInterfaces-CPmSl41C.js";
4
+ import { createRequire } from "node:module";
5
+ import { Publisher, abort, abortIf, assertFound, config, discoverCommands, env, importFile, initializeGlobalContext, loadPrototypes, outputDir, rebuildOutput } from "@arkstack/common";
6
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
4
7
  import { fileURLToPath, pathToFileURL } from "node:url";
5
- import path, { join } from "node:path";
6
- import { MakeResource } from "resora";
7
- import { realpathSync } from "node:fs";
8
+ import path, { dirname, join } from "node:path";
9
+ import { Arkstack } from "@arkstack/contract";
10
+ import { MakeResource, applyRuntimeConfig, getDefaultConfig } from "resora";
8
11
  import { Command, Kernel } from "@h3ravel/musket";
9
12
  import { spawn } from "node:child_process";
13
+ import { randomBytes } from "node:crypto";
10
14
  import { resolve } from "path";
11
15
  import { writeFile } from "fs/promises";
12
16
  import { CliApp as CliApp$1 } from "arkormx";
@@ -15,14 +19,19 @@ import { str } from "@h3ravel/support";
15
19
 
16
20
  //#region dist/commands/BuildCommand.js
17
21
  var BuildCommand = class extends Command {
18
- signature = "build";
22
+ signature = `build
23
+ {--d|dev : Run the build in dev mode, the dev server will not be fired}
24
+ `;
19
25
  description = "Build the application for production";
20
26
  async handle() {
21
27
  await new Promise((resolve, reject) => {
22
28
  const child = spawn(process.platform === "win32" ? "pnpm.cmd" : "pnpm", ["exec", "tsdown"], {
23
- cwd: process.cwd(),
29
+ cwd: Arkstack.rootDir(),
24
30
  stdio: "inherit",
25
- env: Object.assign(process.env, { NODE_ENV: "production" })
31
+ env: Object.assign(process.env, {
32
+ NODE_ENV: this.option("dev") ? "development" : "production",
33
+ CLI_BUILD: this.option("dev") ? "true" : void 0
34
+ })
26
35
  });
27
36
  child.on("error", (error) => {
28
37
  reject(error);
@@ -40,20 +49,32 @@ var BuildCommand = class extends Command {
40
49
 
41
50
  //#endregion
42
51
  //#region dist/commands/DevCommand.js
43
- var DevCommand = class extends Command {
44
- signature = "dev";
52
+ var DevCommand = class DevCommand extends Command {
53
+ signature = `dev
54
+ {--t|tunnel : Tunnel the dev server through Ngrok}
55
+ {--host : Expose the dev server on the local network}
56
+ {--s|secure : Serve the dev server over HTTPS with a self-signed certificate}
57
+ `;
45
58
  description = "Run the development server";
46
59
  async handle() {
60
+ const vars = DevCommand.devServerEnv(this.options());
61
+ const rootDir = Arkstack.rootDir();
62
+ const bin = DevCommand.resolveTsdownBin(rootDir);
63
+ const [command, args] = bin ? [process.execPath, [
64
+ bin,
65
+ "--log-level",
66
+ "silent"
67
+ ]] : [process.platform === "win32" ? "pnpm.cmd" : "pnpm", [
68
+ "exec",
69
+ "tsdown",
70
+ "--log-level",
71
+ "silent"
72
+ ]];
47
73
  await new Promise((resolve, reject) => {
48
- const child = spawn(process.platform === "win32" ? "pnpm.cmd" : "pnpm", [
49
- "exec",
50
- "tsdown",
51
- "--log-level",
52
- "silent"
53
- ], {
54
- cwd: process.cwd(),
74
+ const child = spawn(command, args, {
75
+ cwd: rootDir,
55
76
  stdio: "inherit",
56
- env: Object.assign(process.env, { NODE_ENV: "development" })
77
+ env: Object.assign(process.env, vars)
57
78
  });
58
79
  child.on("error", (error) => {
59
80
  reject(error);
@@ -67,6 +88,111 @@ var DevCommand = class extends Command {
67
88
  });
68
89
  });
69
90
  }
91
+ /**
92
+ * Resolve the absolute path to tsdown's executable so it can be run directly
93
+ * with `node`, skipping the `pnpm exec` wrapper process. Returns `undefined`
94
+ * when tsdown cannot be resolved from the app root, letting the caller fall
95
+ * back to `pnpm exec`.
96
+ *
97
+ * @param rootDir The application root to resolve tsdown from.
98
+ */
99
+ static resolveTsdownBin(rootDir) {
100
+ try {
101
+ const pkgPath = createRequire(join(rootDir, "noop.js")).resolve("tsdown/package.json");
102
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
103
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tsdown;
104
+ return bin ? join(dirname(pkgPath), bin) : void 0;
105
+ } catch {
106
+ return;
107
+ }
108
+ }
109
+ /**
110
+ * Map `dev` command flags to the environment variables the running server reads.
111
+ *
112
+ * The dev server binds `127.0.0.1` by default so it is local-only; `--host`
113
+ * switches it to `0.0.0.0` to expose it on the local network. `--secure` flags
114
+ * the driver to serve HTTPS, and `--tunnel` enables the Ngrok tunnel.
115
+ *
116
+ * @param options
117
+ * @returns
118
+ */
119
+ static devServerEnv(options) {
120
+ const vars = {
121
+ NODE_ENV: "development",
122
+ APP_HOST: options.host ? "0.0.0.0" : "127.0.0.1"
123
+ };
124
+ if (options.tunnel) vars.TUNNEL = "true";
125
+ if (options.secure) vars.APP_SECURE = "true";
126
+ return vars;
127
+ }
128
+ };
129
+
130
+ //#endregion
131
+ //#region dist/commands/KeyGenerateCommand.js
132
+ /**
133
+ * Generate and set the application key (APP_KEY).
134
+ *
135
+ * APP_KEY is the unified secret used for signing JWTs and encrypting values
136
+ * across the framework (exposed as `config('app.key')`).
137
+ */
138
+ var KeyGenerateCommand = class KeyGenerateCommand extends Command {
139
+ signature = `key:generate
140
+ {--show : Display the generated key instead of writing it to the .env file.}
141
+ {--force : Overwrite the existing APP_KEY without confirmation.}
142
+ `;
143
+ description = "Set the application key (APP_KEY).";
144
+ async handle() {
145
+ const key = this.generateKey();
146
+ const ignore = !this.option("interaction");
147
+ if (this.option("show")) {
148
+ this.line(key);
149
+ return;
150
+ }
151
+ const envPath = join(Arkstack.rootDir(), ".env");
152
+ if (!existsSync(envPath)) {
153
+ this.error("No .env file found. Copy .env.example to .env before running key:generate.");
154
+ return;
155
+ }
156
+ const contents = readFileSync(envPath, "utf-8");
157
+ if (KeyGenerateCommand.hasEnvValue(contents, "APP_KEY") && !this.option("force") || ignore) {
158
+ if (!(!ignore ? await this.confirm("An application key already exists. Overwrite it?", false) : false) || ignore) {
159
+ this.info(`Application key generation ${ignore ? "skipped" : "aborted"}.`);
160
+ return;
161
+ }
162
+ }
163
+ writeFileSync(envPath, KeyGenerateCommand.upsertEnvKey(contents, "APP_KEY", key));
164
+ this.success("Application key set successfully.");
165
+ }
166
+ generateKey() {
167
+ return randomBytes(32).toString("base64url");
168
+ }
169
+ /**
170
+ * Whether the env file defines a non-empty value for `name`.
171
+ *
172
+ * An empty assignment (`APP_KEY=`), whitespace, or empty quotes (`APP_KEY=""`)
173
+ * all count as "not set" so a placeholder line is never mistaken for a real key.
174
+ *
175
+ * @param contents The raw `.env` contents.
176
+ * @param name The variable name.
177
+ */
178
+ static hasEnvValue = (contents, name) => {
179
+ const value = contents.match(new RegExp(`^${name}=(.*)$`, "m"))?.[1]?.trim().replace(/^(["'])(.*)\1$/, "$2").trim();
180
+ return Boolean(value);
181
+ };
182
+ /**
183
+ * Return `contents` with `name` set to `value`, replacing the line in place if
184
+ * it exists or appending it otherwise.
185
+ *
186
+ * @param contents The raw `.env` contents.
187
+ * @param name The variable name.
188
+ * @param value The value to set.
189
+ */
190
+ static upsertEnvKey = (contents, name, value) => {
191
+ const line = `${name}=${value}`;
192
+ const pattern = new RegExp(`^${name}=.*$`, "m");
193
+ if (pattern.test(contents)) return contents.replace(pattern, () => line);
194
+ return `${contents.replace(/\s*$/, "")}\n${line}\n`;
195
+ };
70
196
  };
71
197
 
72
198
  //#endregion
@@ -80,7 +206,7 @@ var MakeCommand = class extends Command {
80
206
  const name = String(this.argument("name")).replace(/\s+/g, "").replace(/\.js$/, "").trim();
81
207
  if (!name) return void this.error("Command name is required");
82
208
  const stubContent = this.stub(name);
83
- const filePath = resolve(process.cwd(), "src", `app/console/commands/${name}.js`);
209
+ const filePath = resolve(Arkstack.rootDir(), "src", `app/console/commands/${name}.js`);
84
210
  await writeFile(filePath, stubContent, { flag: "wx" });
85
211
  this.success(`Command ${name} created successfully at ${filePath}`);
86
212
  }
@@ -179,7 +305,129 @@ var MakeFullResource = class extends Command {
179
305
 
180
306
  //#endregion
181
307
  //#region dist/commands/MakeResource.js
182
- var MakeResource$1 = class extends MakeResource {};
308
+ var MakeResource$1 = class extends MakeResource {
309
+ async handle() {
310
+ try {
311
+ applyRuntimeConfig({
312
+ ...getDefaultConfig(),
313
+ ...config("resources", {})
314
+ });
315
+ } catch {}
316
+ return super.handle();
317
+ }
318
+ };
319
+
320
+ //#endregion
321
+ //#region dist/commands/PublishCommand.js
322
+ /** Suffix that marks a file as a publishable stub; stripped on publish. */
323
+ const STUB_SUFFIX = ".stub";
324
+ /**
325
+ * Strip a trailing `.stub` suffix from a path.
326
+ *
327
+ * Stubs are shipped as `<name>.<ext>.stub` so they are ignored by linting,
328
+ * type-checking and test discovery in the source repo, then restored to their
329
+ * real extension when published into an application.
330
+ *
331
+ * @param path
332
+ */
333
+ const stripStubSuffix = (path) => path.endsWith(STUB_SUFFIX) ? path.slice(0, -5) : path;
334
+ /**
335
+ * Publish artifacts (migrations, stubs, assets, …) that installed packages
336
+ * register via `publishes()` into the consuming application.
337
+ */
338
+ var PublishCommand = class extends Command {
339
+ signature = `publish
340
+ {--package= : Only publish artifacts registered by this package (e.g. @arkstack/cache).}
341
+ {--tag= : Only publish artifacts registered under this tag.}
342
+ {--force : Overwrite files that already exist at the destination.}
343
+ {--list : List the publishable artifacts without copying anything.}
344
+ `;
345
+ description = "Publish package artifacts into your application.";
346
+ async handle() {
347
+ await this.loadPackageSetups();
348
+ const filter = {
349
+ package: this.option("package"),
350
+ tag: this.option("tag")
351
+ };
352
+ const groups = Publisher.publishables(filter);
353
+ if (groups.length < 1) {
354
+ this.warn(`No publishable artifacts found${this.describeFilter(filter)}.`);
355
+ return;
356
+ }
357
+ if (this.option("list")) {
358
+ this.listGroups(groups);
359
+ return;
360
+ }
361
+ let published = 0;
362
+ let skipped = 0;
363
+ for (const group of groups) for (const entry of group.entries) {
364
+ if (!existsSync(entry.from)) {
365
+ this.warn(`[${group.package}] Source not found, skipping: ${entry.from}`);
366
+ continue;
367
+ }
368
+ const to = stripStubSuffix(entry.to);
369
+ const dest = join(Arkstack.rootDir(), to);
370
+ if (existsSync(dest) && !this.option("force")) {
371
+ this.warn(`Exists, skipped (use --force): ${to}`);
372
+ skipped++;
373
+ continue;
374
+ }
375
+ mkdirSync(dirname(dest), { recursive: true });
376
+ cpSync(entry.from, dest, { recursive: true });
377
+ if (statSync(dest).isDirectory()) this.stripStubsInTree(dest);
378
+ this.success(`Published [${group.package}] -> ${to}`);
379
+ published++;
380
+ }
381
+ this.info(`Done. ${published} published, ${skipped} skipped.`);
382
+ }
383
+ /**
384
+ * Recursively rename `*.stub` files within a published directory to their
385
+ * real extension.
386
+ *
387
+ * @param dir
388
+ */
389
+ stripStubsInTree(dir) {
390
+ for (const item of readdirSync(dir, {
391
+ recursive: true,
392
+ withFileTypes: true
393
+ })) if (item.isFile() && item.name.endsWith(STUB_SUFFIX)) {
394
+ const current = join(item.parentPath, item.name);
395
+ renameSync(current, stripStubSuffix(current));
396
+ }
397
+ }
398
+ /**
399
+ * Print the publishable artifacts grouped by package without copying.
400
+ *
401
+ * @param groups
402
+ */
403
+ listGroups(groups) {
404
+ this.info("Publishable artifacts:");
405
+ for (const group of groups) {
406
+ this.line(` ${group.package}${group.tag ? ` (tag: ${group.tag})` : ""}`);
407
+ for (const entry of group.entries) this.line(` - ${stripStubSuffix(entry.to)}`);
408
+ }
409
+ }
410
+ /**
411
+ * Import the `setup` module of every installed `@arkstack/*` package so its
412
+ * `publishes()` registrations run. Errors (missing build, side effects) are
413
+ * ignored so one bad package cannot break publishing.
414
+ */
415
+ async loadPackageSetups() {
416
+ const scope = join(Arkstack.rootDir(), "node_modules", "@arkstack");
417
+ if (!existsSync(scope)) return;
418
+ for (const pkg of readdirSync(scope)) {
419
+ const setup = join(scope, pkg, "dist", "setup.js");
420
+ if (!existsSync(setup)) continue;
421
+ try {
422
+ await import(pathToFileURL(setup).href);
423
+ } catch {}
424
+ }
425
+ }
426
+ describeFilter(filter) {
427
+ const parts = [filter.package ? `package "${filter.package}"` : "", filter.tag ? `tag "${filter.tag}"` : ""].filter(Boolean);
428
+ return parts.length ? ` for ${parts.join(" and ")}` : "";
429
+ }
430
+ };
183
431
 
184
432
  //#endregion
185
433
  //#region dist/commands/RouteList.js
@@ -253,14 +501,37 @@ var logo_default = String.raw`
253
501
 
254
502
  //#endregion
255
503
  //#region dist/index.js
256
- /**
257
- * Loads the core application instance by importing the bootstrap file.
504
+ /**
505
+ * A missing-module error from importing a stale/incomplete build artifact.
258
506
  *
507
+ * @param error
259
508
  * @returns
260
509
  */
510
+ const isMissingModuleError = (error) => {
511
+ const code = error?.code;
512
+ return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
513
+ };
514
+ /**
515
+ * Loads the core application instance by importing the built bootstrap file.
516
+ *
517
+ * The kernel boots this for every command — including `build` — before the build
518
+ * can run, so a stale or incomplete build artifact (source changed since the last
519
+ * build: a module moved, renamed, or added) would otherwise wedge startup with
520
+ * `Cannot find module '<outDir>/...'` and the build could never self-heal. When
521
+ * that happens and source is present, regenerate the output once and retry.
522
+ *
523
+ * @returns
524
+ */
261
525
  const loadCoreApp = async () => {
262
- const dist = path.relative(process.cwd(), outputDir());
263
- return (await importFile(join(process.cwd(), `${dist}/core/bootstrap.js`))).app;
526
+ const dist = path.relative(Arkstack.rootDir(), outputDir());
527
+ const bootstrapPath = join(Arkstack.rootDir(), `${dist}/core/bootstrap.js`);
528
+ try {
529
+ return (await importFile(bootstrapPath)).app;
530
+ } catch (error) {
531
+ if (!isMissingModuleError(error) || !existsSync(join(Arkstack.rootDir(), "src"))) throw error;
532
+ await rebuildOutput();
533
+ return (await importFile(bootstrapPath)).app;
534
+ }
264
535
  };
265
536
  /**
266
537
  * Runs the console kernel, initializing the application and registering commands.
@@ -270,13 +541,17 @@ const loadCoreApp = async () => {
270
541
  const runConsoleKernel = async (options = {}) => {
271
542
  loadPrototypes();
272
543
  const app = await loadCoreApp();
273
- const dist = path.relative(process.cwd(), outputDir());
274
544
  const stubsDir = process.env.ARKSTACK_STUBS_DIR;
275
545
  globalThis.app = () => app;
276
546
  globalThis.env = env;
277
547
  globalThis.config = config;
278
548
  globalThis.str = str;
549
+ globalThis.abort = abort;
550
+ globalThis.abortIf = abortIf;
551
+ globalThis.assertFound = assertFound;
279
552
  globalThis.arkctx = { runtime: "CLI" };
553
+ await initializeGlobalContext();
554
+ const userCommands = await discoverCommands();
280
555
  await Kernel.init(await new ArkstackConsoleApp(app, { stubsDir }).loadConfig(), {
281
556
  logo: options.logo ?? logo_default,
282
557
  name: "Cmd",
@@ -287,16 +562,12 @@ const runConsoleKernel = async (options = {}) => {
287
562
  MakeFullResource,
288
563
  DevCommand,
289
564
  BuildCommand,
290
- MakeCommand
291
- ],
292
- discoveryPaths: [
293
- join(process.cwd(), "src", "app", "console", "commands/*.js"),
294
- join(process.cwd(), "src", "app/console/commands/*.js"),
295
- join(process.cwd(), "src", "app/console/commands/*.mjs"),
296
- join(process.cwd(), dist, "app/console/commands/*.js"),
297
- join(process.cwd(), dist, "app/console/commands/*.mjs"),
298
- join(process.cwd(), "node_modules", "@arkstack/*", "dist", "commands", "*.js")
565
+ MakeCommand,
566
+ KeyGenerateCommand,
567
+ PublishCommand,
568
+ ...userCommands
299
569
  ],
570
+ discoveryPaths: [join(Arkstack.rootDir(), "node_modules", "@arkstack/*", "dist", "commands", "*.js")],
300
571
  exceptionHandler(exception) {
301
572
  throw exception;
302
573
  }
@@ -320,4 +591,4 @@ const isEntrypointExecution = () => {
320
591
  if (isEntrypointExecution()) await runConsoleKernel();
321
592
 
322
593
  //#endregion
323
- export { runConsoleKernel };
594
+ export { BaseTCConfig, BuildInterfaces, TSConfig, runConsoleKernel };
package/dist/prepare.js CHANGED
@@ -1,14 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { t as BuildInterfaces } from "./BuildInterfaces-CPmSl41C.js";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import path from "node:path";
5
+ import { Arkstack } from "@arkstack/contract";
1
6
  import { spawn } from "node:child_process";
2
7
  import chalk from "chalk";
3
8
 
4
9
  //#region dist/prepare.js
10
+ if (!existsSync(path.join(Arkstack.rootDir(), ".arkstack/build"))) mkdirSync(path.join(Arkstack.rootDir(), ".arkstack/build"), { recursive: true });
11
+ if (!process.env.NODE_CI) {
12
+ BuildInterfaces.configs();
13
+ BuildInterfaces.env();
14
+ }
15
+ BuildInterfaces.tsconfig();
16
+ const LOG_LEVEL = parseInt(process.env.VERBOSITY ?? "0") > 0 ? [] : ["--log-level=silent"];
5
17
  const NODE_ENV = process.env.NODE_ENV || "development";
6
18
  const child = spawn(process.platform === "win32" ? "pnpm.cmd" : "pnpm", [
7
19
  "exec",
8
20
  "tsdown",
9
- "--log-level=silent"
21
+ ...LOG_LEVEL
10
22
  ], {
11
- cwd: process.cwd(),
23
+ cwd: Arkstack.rootDir(),
12
24
  stdio: "inherit",
13
25
  env: Object.assign({}, process.env, {
14
26
  NODE_ENV,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkstack/console",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "description": "Console module for Arkstack, providing the command-line runtime and console integration layer.",
6
6
  "homepage": "https://arkstack.toneflix.net/guide/cli",
@@ -42,16 +42,17 @@
42
42
  }
43
43
  },
44
44
  "peerDependencies": {
45
- "clear-router": "^2.6.4",
46
- "arkormx": "^2.0.11"
45
+ "arkormx": "^2.10.1",
46
+ "clear-router": "^2.9.0"
47
47
  },
48
48
  "dependencies": {
49
- "@h3ravel/musket": "^0.10.1",
50
- "@h3ravel/support": "^0.15.11",
49
+ "@h3ravel/musket": "^2.2.1",
50
+ "@h3ravel/support": "^2.2.0",
51
51
  "chalk": "^5.6.2",
52
- "resora": "^1.2.4",
53
- "@arkstack/contract": "^0.5.2",
54
- "@arkstack/common": "^0.5.2"
52
+ "resora": "^1.3.27",
53
+ "ts-morph": "^28.0.0",
54
+ "@arkstack/common": "^0.5.3",
55
+ "@arkstack/contract": "^0.5.3"
55
56
  },
56
57
  "scripts": {
57
58
  "build": "tsdown",