@slingr/cli 0.0.2 ā 0.0.4
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/LICENSE.txt +202 -0
- package/README.md +490 -319
- package/bin/dev.cmd +2 -2
- package/bin/dev.js +5 -5
- package/bin/run.cmd +2 -2
- package/bin/run.js +4 -4
- package/bin/slingr +1 -0
- package/dist/commands/build.d.ts +20 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +206 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/create-app.d.ts +0 -1
- package/dist/commands/create-app.d.ts.map +1 -1
- package/dist/commands/create-app.js +38 -57
- package/dist/commands/create-app.js.map +1 -1
- package/dist/commands/debug.d.ts +28 -0
- package/dist/commands/debug.d.ts.map +1 -0
- package/dist/commands/debug.js +474 -0
- package/dist/commands/debug.js.map +1 -0
- package/dist/commands/ds.d.ts +14 -1
- package/dist/commands/ds.d.ts.map +1 -1
- package/dist/commands/ds.js +450 -121
- package/dist/commands/ds.js.map +1 -1
- package/dist/commands/gql.d.ts +1 -1
- package/dist/commands/gql.d.ts.map +1 -1
- package/dist/commands/gql.js +190 -184
- package/dist/commands/gql.js.map +1 -1
- package/dist/commands/infra/down.d.ts.map +1 -1
- package/dist/commands/infra/down.js +8 -7
- package/dist/commands/infra/down.js.map +1 -1
- package/dist/commands/infra/up.d.ts.map +1 -1
- package/dist/commands/infra/up.js +8 -7
- package/dist/commands/infra/up.js.map +1 -1
- package/dist/commands/infra/update.d.ts +1 -0
- package/dist/commands/infra/update.d.ts.map +1 -1
- package/dist/commands/infra/update.js +33 -69
- package/dist/commands/infra/update.js.map +1 -1
- package/dist/commands/run.d.ts +29 -2
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +628 -130
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +34 -71
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/sync-metadata.d.ts +15 -0
- package/dist/commands/sync-metadata.d.ts.map +1 -0
- package/dist/commands/sync-metadata.js +225 -0
- package/dist/commands/sync-metadata.js.map +1 -0
- package/dist/commands/users.d.ts +30 -0
- package/dist/commands/users.d.ts.map +1 -0
- package/dist/commands/users.js +472 -0
- package/dist/commands/users.js.map +1 -0
- package/dist/commands/views.d.ts +11 -0
- package/dist/commands/views.d.ts.map +1 -0
- package/dist/commands/views.js +73 -0
- package/dist/commands/views.js.map +1 -0
- package/dist/projectStructure.d.ts +2 -2
- package/dist/projectStructure.d.ts.map +1 -1
- package/dist/projectStructure.js +281 -69
- package/dist/projectStructure.js.map +1 -1
- package/dist/scripts/generate-metadata.d.ts +13 -0
- package/dist/scripts/generate-metadata.d.ts.map +1 -0
- package/dist/scripts/generate-metadata.js +412 -0
- package/dist/scripts/generate-metadata.js.map +1 -0
- package/dist/scripts/generate-metadata.ts +498 -0
- package/dist/scripts/generate-schema.d.ts +1 -1
- package/dist/scripts/generate-schema.js +168 -74
- package/dist/scripts/generate-schema.js.map +1 -1
- package/dist/scripts/generate-schema.ts +258 -143
- package/dist/templates/.env.template +23 -0
- package/dist/templates/.firebaserc.template +5 -0
- package/dist/templates/.github/copilot-instructions.md.template +652 -17
- package/dist/templates/backend/Dockerfile.template +30 -0
- package/dist/templates/config/datasource.ts.template +12 -9
- package/dist/templates/config/jest.config.ts +30 -30
- package/dist/templates/config/jest.setup.ts +1 -1
- package/dist/templates/config/tsconfig.json.template +50 -29
- package/dist/templates/dataSources/mysql.ts.template +16 -13
- package/dist/templates/dataSources/postgres.ts.template +15 -13
- package/dist/templates/dataset-generator-script.ts.template +139 -139
- package/dist/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/dist/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/dist/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/dist/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/dist/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/dist/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/dist/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/dist/templates/docker-compose.prod-test.yml.template +32 -0
- package/dist/templates/docker-compose.yml.template +24 -0
- package/dist/templates/docs/app-description.md.template +33 -33
- package/dist/templates/firebase.json.template +68 -0
- package/dist/templates/frontend/.umirc.ts.template +23 -0
- package/dist/templates/frontend/package.json.template +45 -0
- package/dist/templates/frontend/public/config.json +6 -0
- package/dist/templates/frontend/public/logo.svg +6 -0
- package/dist/templates/frontend/src/app.tsx.template +44 -0
- package/dist/templates/frontend/src/global.less.template +117 -0
- package/dist/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/dist/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/dist/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/dist/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/dist/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/dist/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/dist/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/dist/templates/frontend/tsconfig.json.template +50 -0
- package/dist/templates/gql/codegen.yml.template +25 -25
- package/dist/templates/gql/index.ts.template +17 -24
- package/dist/templates/gql/operations.graphql.template +30 -30
- package/dist/templates/ops/README.md.template +1045 -0
- package/dist/templates/ops/cloudbuild.yaml.template +161 -0
- package/dist/templates/ops/scripts/_utils.js.template +217 -0
- package/dist/templates/ops/scripts/deploy.js.template +145 -0
- package/dist/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/dist/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/dist/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/dist/templates/package.json.template +50 -38
- package/dist/templates/pnpm-workspace.yaml.template +3 -0
- package/dist/templates/prompt-analysis.md.template +110 -110
- package/dist/templates/prompt-script-generation.md.template +258 -258
- package/dist/templates/src/Address.ts.template +28 -31
- package/dist/templates/src/App.ts.template +17 -61
- package/dist/templates/src/Company.ts.template +41 -47
- package/dist/templates/src/Models.test.ts.template +654 -654
- package/dist/templates/src/Person.test.ts.template +289 -289
- package/dist/templates/src/Person.ts.template +90 -105
- package/dist/templates/src/actions/index.ts.template +11 -11
- package/dist/templates/src/auth/permissions.ts.template +34 -0
- package/dist/templates/src/data/App.ts.template +48 -0
- package/dist/templates/src/data/User.ts.template +35 -0
- package/dist/templates/src/types/gql.d.ts.template +17 -17
- package/dist/templates/vscode/extensions.json +4 -3
- package/dist/templates/vscode/settings.json +17 -11
- package/dist/templates/workspace-package.json.template +21 -0
- package/dist/utils/buildCache.d.ts +12 -0
- package/dist/utils/buildCache.d.ts.map +1 -0
- package/dist/utils/buildCache.js +102 -0
- package/dist/utils/buildCache.js.map +1 -0
- package/dist/utils/checkFramework.d.ts +27 -0
- package/dist/utils/checkFramework.d.ts.map +1 -0
- package/dist/utils/checkFramework.js +104 -0
- package/dist/utils/checkFramework.js.map +1 -0
- package/dist/utils/datasourceParser.d.ts +11 -0
- package/dist/utils/datasourceParser.d.ts.map +1 -1
- package/dist/utils/datasourceParser.js +154 -56
- package/dist/utils/datasourceParser.js.map +1 -1
- package/dist/utils/dockerManager.d.ts +25 -0
- package/dist/utils/dockerManager.d.ts.map +1 -0
- package/dist/utils/dockerManager.js +281 -0
- package/dist/utils/dockerManager.js.map +1 -0
- package/dist/utils/infraFileParser.d.ts +26 -0
- package/dist/utils/infraFileParser.d.ts.map +1 -0
- package/dist/utils/infraFileParser.js +75 -0
- package/dist/utils/infraFileParser.js.map +1 -0
- package/dist/utils/jsonlLoader.d.ts +91 -12
- package/dist/utils/jsonlLoader.d.ts.map +1 -1
- package/dist/utils/jsonlLoader.js +674 -63
- package/dist/utils/jsonlLoader.js.map +1 -1
- package/dist/utils/model-analyzer.d.ts.map +1 -1
- package/dist/utils/model-analyzer.js +67 -13
- package/dist/utils/model-analyzer.js.map +1 -1
- package/dist/utils/userManagement.d.ts +57 -0
- package/dist/utils/userManagement.d.ts.map +1 -0
- package/dist/utils/userManagement.js +288 -0
- package/dist/utils/userManagement.js.map +1 -0
- package/dist/utils/viewsGenerator.d.ts +15 -0
- package/dist/utils/viewsGenerator.d.ts.map +1 -0
- package/dist/utils/viewsGenerator.js +311 -0
- package/dist/utils/viewsGenerator.js.map +1 -0
- package/oclif.manifest.json +445 -20
- package/package.json +29 -27
- package/src/templates/.env.template +23 -0
- package/src/templates/.firebaserc.template +5 -0
- package/src/templates/.github/copilot-instructions.md.template +652 -17
- package/src/templates/backend/Dockerfile.template +30 -0
- package/src/templates/config/datasource.ts.template +12 -9
- package/src/templates/config/jest.config.ts +30 -30
- package/src/templates/config/jest.setup.ts +1 -1
- package/src/templates/config/tsconfig.json.template +50 -29
- package/src/templates/dataSources/mysql.ts.template +16 -13
- package/src/templates/dataSources/postgres.ts.template +15 -13
- package/src/templates/dataset-generator-script.ts.template +139 -139
- package/src/templates/datasets/mysql-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/mysql-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/App.jsonl.template +4 -4
- package/src/templates/datasets/mysql-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/mysql-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/mysql-default/User.jsonl.template +1 -0
- package/src/templates/datasets/mysql-default/instructions.md.template +1 -0
- package/src/templates/datasets/postgres-default/.slingr-schema.json.template +5 -0
- package/src/templates/datasets/postgres-default/Address.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/App.jsonl.template +4 -4
- package/src/templates/datasets/postgres-default/Company.jsonl.template +3 -3
- package/src/templates/datasets/postgres-default/Person.jsonl.template +2 -2
- package/src/templates/datasets/postgres-default/User.jsonl.template +1 -0
- package/src/templates/datasets/postgres-default/instructions.md.template +1 -0
- package/src/templates/docker-compose.prod-test.yml.template +32 -0
- package/src/templates/docker-compose.yml.template +24 -0
- package/src/templates/docs/app-description.md.template +33 -33
- package/src/templates/firebase.json.template +68 -0
- package/src/templates/frontend/.umirc.ts.template +23 -0
- package/src/templates/frontend/package.json.template +45 -0
- package/src/templates/frontend/public/config.json +6 -0
- package/src/templates/frontend/public/logo.svg +6 -0
- package/src/templates/frontend/src/app.tsx.template +44 -0
- package/src/templates/frontend/src/global.less.template +117 -0
- package/src/templates/frontend/src/layouts/MainLayout.tsx.template +75 -0
- package/src/templates/frontend/src/types/graphql-augmentation.d.ts.template +44 -0
- package/src/templates/frontend/src/views/customViews/user/UserCreateView.tsx.template +18 -0
- package/src/templates/frontend/src/views/customViews/user/UserEditView.tsx.template +29 -0
- package/src/templates/frontend/src/views/customViews/user/UserReadView.tsx.template +24 -0
- package/src/templates/frontend/src/views/customViews/user/UserTableView.tsx.template +38 -0
- package/src/templates/frontend/src/views/customViews/welcome.tsx.template +34 -0
- package/src/templates/frontend/tsconfig.json.template +50 -0
- package/src/templates/gql/codegen.yml.template +25 -25
- package/src/templates/gql/index.ts.template +17 -24
- package/src/templates/gql/operations.graphql.template +30 -30
- package/src/templates/ops/README.md.template +1045 -0
- package/src/templates/ops/cloudbuild.yaml.template +161 -0
- package/src/templates/ops/scripts/_utils.js.template +217 -0
- package/src/templates/ops/scripts/deploy.js.template +145 -0
- package/src/templates/ops/scripts/setup-gcp.js.template +330 -0
- package/src/templates/ops/scripts/setup-secrets.js.template +76 -0
- package/src/templates/ops/scripts/test-prod-local.js.template +49 -0
- package/src/templates/package.json.template +50 -38
- package/src/templates/pnpm-workspace.yaml.template +3 -0
- package/src/templates/prompt-analysis.md.template +110 -110
- package/src/templates/prompt-script-generation.md.template +258 -258
- package/src/templates/src/Address.ts.template +28 -31
- package/src/templates/src/App.ts.template +17 -61
- package/src/templates/src/Company.ts.template +41 -47
- package/src/templates/src/Models.test.ts.template +654 -654
- package/src/templates/src/Person.test.ts.template +289 -289
- package/src/templates/src/Person.ts.template +90 -105
- package/src/templates/src/actions/index.ts.template +11 -11
- package/src/templates/src/auth/permissions.ts.template +34 -0
- package/src/templates/src/data/App.ts.template +48 -0
- package/src/templates/src/data/User.ts.template +35 -0
- package/src/templates/src/types/gql.d.ts.template +17 -17
- package/src/templates/vscode/extensions.json +4 -3
- package/src/templates/vscode/settings.json +17 -11
- package/src/templates/workspace-package.json.template +21 -0
- package/dist/templates/src/index.ts +0 -66
- package/src/templates/src/index.ts +0 -66
package/dist/commands/run.js
CHANGED
|
@@ -9,22 +9,121 @@ const node_child_process_1 = require("node:child_process");
|
|
|
9
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
10
|
const datasourceParser_js_1 = require("../utils/datasourceParser.js");
|
|
11
11
|
const portChecker_js_1 = require("../utils/portChecker.js");
|
|
12
|
+
const checkFramework_js_1 = require("../utils/checkFramework.js");
|
|
13
|
+
const viewsGenerator_js_1 = require("../utils/viewsGenerator.js");
|
|
14
|
+
const buildCache_js_1 = require("../utils/buildCache.js");
|
|
12
15
|
class Run extends core_1.Command {
|
|
13
|
-
static description = 'Run a Slingr application locally'
|
|
16
|
+
static description = 'Run a Slingr application locally with both backend and frontend on a single port.\n\n' +
|
|
17
|
+
'By default, runs in production mode (unified server). Use --dev flag for hot-reload development with separate processes.';
|
|
14
18
|
static examples = [
|
|
15
19
|
'<%= config.bin %> <%= command.id %>',
|
|
16
20
|
'<%= config.bin %> <%= command.id %> --skip-infra',
|
|
21
|
+
'<%= config.bin %> <%= command.id %> --dev',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> --ui-only',
|
|
23
|
+
'<%= config.bin %> <%= command.id %> --backend',
|
|
17
24
|
];
|
|
18
25
|
static flags = {
|
|
19
26
|
help: core_1.Flags.help({ char: 'h' }),
|
|
20
27
|
'skip-infra': core_1.Flags.boolean({
|
|
21
28
|
char: 'i',
|
|
22
|
-
description: 'Skip infrastructure setup and checks',
|
|
29
|
+
description: 'Skip infrastructure setup and checks (use when database containers are already running)',
|
|
30
|
+
required: false,
|
|
31
|
+
}),
|
|
32
|
+
dev: core_1.Flags.boolean({
|
|
33
|
+
char: 'd',
|
|
34
|
+
description: 'Run in development mode with hot-reload (starts UI dev server and backend separately). Mutually exclusive with --ui-only and --backend.',
|
|
35
|
+
required: false,
|
|
36
|
+
exclusive: ['ui-only', 'backend'],
|
|
37
|
+
}),
|
|
38
|
+
'ui-only': core_1.Flags.boolean({
|
|
39
|
+
description: 'Start only the UI development server (backend must be running separately). Mutually exclusive with --dev and --backend.',
|
|
40
|
+
required: false,
|
|
41
|
+
exclusive: ['dev', 'backend'],
|
|
42
|
+
}),
|
|
43
|
+
backend: core_1.Flags.boolean({
|
|
44
|
+
char: 'b',
|
|
45
|
+
description: 'Start only the backend server (use with --ui-only for split development). Mutually exclusive with --dev and --ui-only.',
|
|
46
|
+
required: false,
|
|
47
|
+
exclusive: ['dev', 'ui-only'],
|
|
48
|
+
}),
|
|
49
|
+
port: core_1.Flags.integer({
|
|
50
|
+
char: 'p',
|
|
51
|
+
description: 'Custom port for the server (default: 3000 or first available)',
|
|
52
|
+
required: false,
|
|
53
|
+
}),
|
|
54
|
+
verbose: core_1.Flags.boolean({
|
|
55
|
+
char: 'v',
|
|
56
|
+
description: 'Show detailed build and setup output and enable DEBUG level logs',
|
|
23
57
|
required: false,
|
|
24
58
|
}),
|
|
25
59
|
};
|
|
60
|
+
verbose = false;
|
|
61
|
+
stepResults = [];
|
|
62
|
+
summarized = false;
|
|
63
|
+
/** Log only when --verbose is active. */
|
|
64
|
+
vlog(msg) {
|
|
65
|
+
if (this.verbose) {
|
|
66
|
+
this.log(msg);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Record the result of a step for the final summary. */
|
|
70
|
+
recordStep(name, status, message) {
|
|
71
|
+
const step = { name, status };
|
|
72
|
+
if (message !== undefined)
|
|
73
|
+
step.message = message;
|
|
74
|
+
this.stepResults.push(step);
|
|
75
|
+
}
|
|
76
|
+
/** Extract build output from an execSync error (includes both stdout and stderr). */
|
|
77
|
+
extractBuildOutput(error) {
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (error?.stdout) {
|
|
80
|
+
const stdout = error.stdout.toString().trim();
|
|
81
|
+
if (stdout)
|
|
82
|
+
parts.push(stdout);
|
|
83
|
+
}
|
|
84
|
+
if (error?.stderr) {
|
|
85
|
+
const stderr = error.stderr.toString().trim();
|
|
86
|
+
if (stderr)
|
|
87
|
+
parts.push(stderr);
|
|
88
|
+
}
|
|
89
|
+
return parts.length > 0 ? '\n' + parts.join('\n') : '';
|
|
90
|
+
}
|
|
91
|
+
/** Print a summary of all recorded steps. */
|
|
92
|
+
printSummary() {
|
|
93
|
+
if (this.stepResults.length === 0 || this.summarized)
|
|
94
|
+
return;
|
|
95
|
+
this.summarized = true;
|
|
96
|
+
const hasErrors = this.stepResults.some(s => s.status === 'error');
|
|
97
|
+
const hasWarnings = this.stepResults.some(s => s.status === 'warning');
|
|
98
|
+
this.log('');
|
|
99
|
+
this.log('ā'.repeat(60));
|
|
100
|
+
if (hasErrors) {
|
|
101
|
+
this.log('ā Process completed with errors');
|
|
102
|
+
}
|
|
103
|
+
else if (hasWarnings) {
|
|
104
|
+
this.log('ā ļø Process completed with warnings');
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
this.log('ā
All steps completed successfully');
|
|
108
|
+
}
|
|
109
|
+
this.log('ā'.repeat(60));
|
|
110
|
+
for (const step of this.stepResults) {
|
|
111
|
+
const icon = step.status === 'success' ? 'ā
'
|
|
112
|
+
: step.status === 'warning' ? 'ā ļø '
|
|
113
|
+
: step.status === 'skipped' ? 'āļø '
|
|
114
|
+
: 'ā';
|
|
115
|
+
const msg = step.status === 'skipped'
|
|
116
|
+
? ` ā skipped${step.message ? ` (${step.message})` : ''}`
|
|
117
|
+
: step.message ? ` ā ${step.message}` : '';
|
|
118
|
+
this.log(` ${icon} ${step.name}${msg}`);
|
|
119
|
+
}
|
|
120
|
+
this.log('ā'.repeat(60));
|
|
121
|
+
this.log('');
|
|
122
|
+
}
|
|
26
123
|
async run() {
|
|
27
124
|
const { flags } = await this.parse(Run);
|
|
125
|
+
this.verbose = flags.verbose ?? false;
|
|
126
|
+
process.env.SLINGR_VERBOSE = this.verbose ? 'true' : 'false';
|
|
28
127
|
try {
|
|
29
128
|
// Check if we're in a Slingr app directory
|
|
30
129
|
const packageJsonPath = node_path_1.default.join(process.cwd(), 'package.json');
|
|
@@ -32,154 +131,144 @@ class Run extends core_1.Command {
|
|
|
32
131
|
this.error("Not in a Slingr application directory. Please run this command from your app's root directory.");
|
|
33
132
|
}
|
|
34
133
|
const packageJson = await fs_extra_1.default.readJSON(packageJsonPath);
|
|
35
|
-
if (!
|
|
134
|
+
if (!(await (0, checkFramework_js_1.hasSlingrFramework)())) {
|
|
36
135
|
this.error('This directory does not contain a Slingr application.');
|
|
37
136
|
}
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
137
|
+
// Verify entrypoint exists
|
|
138
|
+
const backendPath = (0, checkFramework_js_1.getBackendPath)();
|
|
139
|
+
const srcAppPath = node_path_1.default.join(backendPath, 'src', 'App.ts');
|
|
140
|
+
if (!(await fs_extra_1.default.pathExists(srcAppPath))) {
|
|
141
|
+
this.error('backend/src/App.ts not found. This does not appear to be a valid Slingr application.');
|
|
42
142
|
}
|
|
43
|
-
//
|
|
143
|
+
// 1. Build/Prepare Framework (Conditioned)
|
|
44
144
|
await this.buildFramework();
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
145
|
+
// 2. Execution Mode
|
|
146
|
+
if (flags['ui-only']) {
|
|
147
|
+
await this.runUIOnly(flags.port);
|
|
148
|
+
}
|
|
149
|
+
else if (flags.backend) {
|
|
150
|
+
await this.runBackendOnly(flags);
|
|
151
|
+
}
|
|
152
|
+
else if (flags.dev) {
|
|
153
|
+
await this.runDevMode(flags);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Production / Default Unified Mode - needs UI
|
|
157
|
+
await (0, viewsGenerator_js_1.generateViewsContext)(process.cwd(), this.verbose);
|
|
158
|
+
await this.buildUI();
|
|
159
|
+
await this.generateCode();
|
|
160
|
+
if (!flags['skip-infra']) {
|
|
161
|
+
await this.checkInfrastructure();
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
this.recordStep('Infrastructure', 'skipped', '--skip-infra');
|
|
165
|
+
}
|
|
166
|
+
this.printSummary();
|
|
167
|
+
await this.executeApp(flags.port, {}, true); // true = production mode
|
|
52
168
|
}
|
|
53
|
-
// Step 4: Execute the application
|
|
54
|
-
await this.executeApp();
|
|
55
169
|
}
|
|
56
170
|
catch (error) {
|
|
171
|
+
this.printSummary();
|
|
57
172
|
this.error(error.message);
|
|
58
173
|
}
|
|
59
174
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Helper to resolve where the compiled UI is located.
|
|
177
|
+
*/
|
|
178
|
+
async resolveUiPath() {
|
|
179
|
+
const cwd = process.cwd();
|
|
180
|
+
// Check for app's frontend/dist directory
|
|
181
|
+
const frontendDistPath = node_path_1.default.join(cwd, 'frontend', 'dist');
|
|
182
|
+
if (await fs_extra_1.default.pathExists(node_path_1.default.join(frontendDistPath, 'index.html'))) {
|
|
183
|
+
return { path: frontendDistPath, exists: true };
|
|
68
184
|
}
|
|
69
|
-
//
|
|
70
|
-
|
|
185
|
+
// UI not found
|
|
186
|
+
return { path: frontendDistPath, exists: false };
|
|
187
|
+
}
|
|
188
|
+
async buildFramework(silent = false) {
|
|
189
|
+
// Only attempts to build if it detects it's a local linked dependency,
|
|
190
|
+
// otherwise just verifies existence.
|
|
71
191
|
try {
|
|
72
|
-
require.resolve('slingr-
|
|
73
|
-
if (!silent) {
|
|
74
|
-
this.log('ā
slingr-
|
|
192
|
+
const frameworkPath = require.resolve('@slingr/framework-backend');
|
|
193
|
+
if (frameworkPath && !silent && this.verbose) {
|
|
194
|
+
this.log('ā
@slingr/framework-backend is available');
|
|
75
195
|
}
|
|
76
196
|
}
|
|
77
197
|
catch (error) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
async checkGraphQLSetup() {
|
|
82
|
-
const schemaPath = node_path_1.default.join(process.cwd(), 'generated', 'gql', 'schema.graphql');
|
|
83
|
-
const sdkPath = node_path_1.default.join(process.cwd(), 'generated', 'gql', 'sdk.ts');
|
|
84
|
-
const indexPath = node_path_1.default.join(process.cwd(), 'generated', 'gql', 'index.ts');
|
|
85
|
-
const schemaExists = await fs_extra_1.default.pathExists(schemaPath);
|
|
86
|
-
const sdkExists = await fs_extra_1.default.pathExists(sdkPath);
|
|
87
|
-
const indexExists = await fs_extra_1.default.pathExists(indexPath);
|
|
88
|
-
if (!schemaExists || !sdkExists || !indexExists) {
|
|
89
|
-
this.log('');
|
|
90
|
-
this.log('ā ļø GraphQL schema and SDK not found!');
|
|
91
|
-
this.log('');
|
|
92
|
-
this.log('The GraphQL server requires a generated schema and SDK to run.');
|
|
93
|
-
this.log('');
|
|
94
|
-
if (!schemaExists) {
|
|
95
|
-
this.log('ā Missing: generated/gql/schema.graphql');
|
|
96
|
-
}
|
|
97
|
-
if (!sdkExists) {
|
|
98
|
-
this.log('ā Missing: generated/gql/sdk.ts');
|
|
99
|
-
}
|
|
100
|
-
if (!indexExists) {
|
|
101
|
-
this.log('ā Missing: generated/gql/index.ts');
|
|
198
|
+
const nodeModulesPath = node_path_1.default.join(process.cwd(), 'node_modules', '@slingr');
|
|
199
|
+
if (!(await fs_extra_1.default.pathExists(nodeModulesPath))) {
|
|
200
|
+
this.error('@slingr/framework-backend not found. Please run npm install.');
|
|
102
201
|
}
|
|
103
|
-
this.log('');
|
|
104
|
-
this.log('š To generate the required files, run:');
|
|
105
|
-
this.log('');
|
|
106
|
-
this.log(' 1. Generate GraphQL schema:');
|
|
107
|
-
this.log(' $ slingr gql generate-schema');
|
|
108
|
-
this.log('');
|
|
109
|
-
this.log(' 2. Generate SDK:');
|
|
110
|
-
this.log(' $ slingr gql generate-sdk');
|
|
111
|
-
this.log('');
|
|
112
|
-
this.log(' 3. Then run the server:');
|
|
113
|
-
this.log(' $ slingr run');
|
|
114
|
-
this.log('');
|
|
115
|
-
this.error('GraphQL setup incomplete. Please run the commands above.');
|
|
116
202
|
}
|
|
117
|
-
this.log('ā
GraphQL schema and SDK found');
|
|
118
203
|
}
|
|
119
204
|
async checkInfrastructure() {
|
|
120
205
|
const dataSources = await this.loadDataSources();
|
|
121
|
-
// Check
|
|
206
|
+
// Check Docker availability
|
|
122
207
|
try {
|
|
123
208
|
(0, node_child_process_1.execSync)('docker --version', { stdio: 'pipe' });
|
|
124
209
|
}
|
|
125
210
|
catch {
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
// Check if Docker Engine is running BEFORE checking ports
|
|
129
|
-
if (!(0, portChecker_js_1.isDockerRunning)()) {
|
|
130
|
-
this.error('š³ Docker is not running. Please start Docker and try again.\n\n' +
|
|
131
|
-
'On macOS/Windows: Start Docker Desktop\n' +
|
|
132
|
-
'On Linux: Run "sudo systemctl start docker" or "sudo service docker start"');
|
|
211
|
+
this.recordStep('Infrastructure', 'error', 'Docker is not installed');
|
|
212
|
+
this.error('Docker is not installed.');
|
|
133
213
|
}
|
|
134
|
-
|
|
214
|
+
const { ensureDockerIsRunning } = await import('../utils/dockerManager.js');
|
|
135
215
|
try {
|
|
136
|
-
|
|
216
|
+
await ensureDockerIsRunning('slingr run');
|
|
137
217
|
}
|
|
138
|
-
catch {
|
|
139
|
-
this.
|
|
218
|
+
catch (error) {
|
|
219
|
+
this.recordStep('Infrastructure', 'error', error.message);
|
|
220
|
+
this.error(error.message);
|
|
140
221
|
}
|
|
141
|
-
// Check port availability before starting services
|
|
142
222
|
await this.checkPortAvailability();
|
|
143
|
-
// Run infra update command to ensure latest infrastructure configuration
|
|
144
223
|
await this.config.runCommand('infra:update', ['--all']);
|
|
145
|
-
|
|
146
|
-
this.log('Starting infrastructure services...');
|
|
224
|
+
this.vlog('Starting infrastructure services...');
|
|
147
225
|
try {
|
|
148
|
-
(0, node_child_process_1.execSync)('docker compose up -d', { stdio: 'inherit' });
|
|
226
|
+
(0, node_child_process_1.execSync)('docker compose up -d', { stdio: this.verbose ? 'inherit' : 'pipe' });
|
|
149
227
|
}
|
|
150
228
|
catch {
|
|
151
|
-
this.
|
|
229
|
+
this.recordStep('Infrastructure', 'error', 'Failed to start Docker services');
|
|
230
|
+
this.error('Failed to start Docker services.');
|
|
152
231
|
}
|
|
153
|
-
//
|
|
154
|
-
this.
|
|
232
|
+
// Health check loop
|
|
233
|
+
this.vlog('Waiting for services to be ready...');
|
|
234
|
+
let infraWarnings = [];
|
|
155
235
|
for (const ds of dataSources) {
|
|
156
236
|
const serviceName = `${ds.name}-db`;
|
|
157
|
-
this.log(`Checking ${serviceName}...`);
|
|
158
237
|
let attempts = 0;
|
|
159
238
|
const maxAttempts = 30;
|
|
239
|
+
if (this.verbose) {
|
|
240
|
+
process.stdout.write(`Checking ${serviceName}... `);
|
|
241
|
+
}
|
|
160
242
|
while (attempts < maxAttempts) {
|
|
161
243
|
try {
|
|
162
244
|
const containerInfo = (0, node_child_process_1.execSync)(`docker ps -f name=${serviceName} --format '{{.Status}}'`, {
|
|
163
245
|
encoding: 'utf-8',
|
|
164
246
|
});
|
|
165
247
|
if (containerInfo.includes('healthy')) {
|
|
166
|
-
this.
|
|
248
|
+
if (this.verbose) {
|
|
249
|
+
console.log('ā
');
|
|
250
|
+
}
|
|
167
251
|
break;
|
|
168
252
|
}
|
|
169
253
|
}
|
|
170
|
-
catch {
|
|
171
|
-
// Continue trying
|
|
172
|
-
}
|
|
254
|
+
catch { }
|
|
173
255
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
174
256
|
attempts++;
|
|
175
|
-
if (attempts === maxAttempts) {
|
|
176
|
-
this.error(`Service ${serviceName} is not healthy after ${maxAttempts} seconds`);
|
|
177
|
-
}
|
|
178
257
|
}
|
|
258
|
+
if (attempts === maxAttempts) {
|
|
259
|
+
this.warn(`Service ${serviceName} is not reporting healthy yet.`);
|
|
260
|
+
infraWarnings.push(`${serviceName} not healthy`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (infraWarnings.length > 0) {
|
|
264
|
+
this.recordStep('Infrastructure', 'warning', infraWarnings.join(', '));
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
this.recordStep('Infrastructure', 'success');
|
|
179
268
|
}
|
|
180
269
|
}
|
|
181
270
|
async checkPortAvailability() {
|
|
182
|
-
this.
|
|
271
|
+
this.vlog('Checking port availability for datasources...');
|
|
183
272
|
const dataSourcePorts = await (0, datasourceParser_js_1.extractDataSourcePorts)();
|
|
184
273
|
if (dataSourcePorts.length === 0) {
|
|
185
274
|
return;
|
|
@@ -188,8 +277,8 @@ class Run extends core_1.Command {
|
|
|
188
277
|
const portUsage = await (0, portChecker_js_1.checkPortsUsage)(ports);
|
|
189
278
|
const conflictingPorts = portUsage.filter(p => p.inUse && !p.isProjectDocker);
|
|
190
279
|
const dockerPorts = portUsage.filter(p => p.inUse && p.isProjectDocker);
|
|
191
|
-
// Show info about existing Docker containers
|
|
192
|
-
if (dockerPorts.length > 0) {
|
|
280
|
+
// Show info about existing Docker containers (verbose only)
|
|
281
|
+
if (dockerPorts.length > 0 && this.verbose) {
|
|
193
282
|
this.log('ā¹ļø Found existing project containers:');
|
|
194
283
|
for (const dockerPort of dockerPorts) {
|
|
195
284
|
const dataSource = dataSourcePorts.find(ds => ds.port === dockerPort.port);
|
|
@@ -200,6 +289,7 @@ class Run extends core_1.Command {
|
|
|
200
289
|
this.log('');
|
|
201
290
|
}
|
|
202
291
|
if (conflictingPorts.length > 0) {
|
|
292
|
+
// Conflict info is always shown ā it precedes a fatal error
|
|
203
293
|
this.log('ā ļø Port conflicts detected!');
|
|
204
294
|
this.log('');
|
|
205
295
|
for (const conflictPort of conflictingPorts) {
|
|
@@ -230,36 +320,375 @@ class Run extends core_1.Command {
|
|
|
230
320
|
this.error(`Cannot start infrastructure due to port conflicts. Please resolve the port conflicts above.`);
|
|
231
321
|
}
|
|
232
322
|
else {
|
|
233
|
-
this.
|
|
323
|
+
this.vlog('ā
All required ports are available');
|
|
234
324
|
}
|
|
235
325
|
}
|
|
236
|
-
async
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
326
|
+
async buildUI() {
|
|
327
|
+
const frontendPath = node_path_1.default.join(process.cwd(), 'frontend');
|
|
328
|
+
// Check if frontend directory exists
|
|
329
|
+
if (!(await fs_extra_1.default.pathExists(frontendPath))) {
|
|
330
|
+
this.error('Frontend directory not found. This application does not have a frontend project.\n\n' +
|
|
331
|
+
'Expected frontend directory at: ' +
|
|
332
|
+
frontendPath +
|
|
333
|
+
'\n\n' +
|
|
334
|
+
'Please ensure your Slingr application has a frontend/ directory.');
|
|
335
|
+
}
|
|
336
|
+
// Check if frontend/package.json exists
|
|
337
|
+
const frontendPackageJson = node_path_1.default.join(frontendPath, 'package.json');
|
|
338
|
+
if (!(await fs_extra_1.default.pathExists(frontendPackageJson))) {
|
|
339
|
+
this.error('Frontend package.json not found.\n\n' + 'Expected at: ' + frontendPackageJson);
|
|
340
|
+
}
|
|
341
|
+
const uiInfo = await this.resolveUiPath();
|
|
342
|
+
if (uiInfo.exists) {
|
|
343
|
+
this.vlog('ā
Using pre-built UI from frontend/dist');
|
|
344
|
+
this.recordStep('Frontend build', 'skipped', 'Using pre-built UI');
|
|
243
345
|
}
|
|
346
|
+
else {
|
|
347
|
+
this.vlog('š¦ Building frontend...');
|
|
348
|
+
try {
|
|
349
|
+
if (this.verbose) {
|
|
350
|
+
// Stream build output directly to the console in verbose mode
|
|
351
|
+
(0, node_child_process_1.execSync)('npm run build', { stdio: 'inherit', cwd: frontendPath });
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
// Capture output for potential error reporting in non-verbose mode
|
|
355
|
+
(0, node_child_process_1.execSync)('npm run build', { stdio: 'pipe', cwd: frontendPath });
|
|
356
|
+
}
|
|
357
|
+
this.vlog('ā
Frontend built successfully');
|
|
358
|
+
this.recordStep('Frontend build', 'success');
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
const detail = this.extractBuildOutput(error);
|
|
362
|
+
this.recordStep('Frontend build', 'error', 'Compilation failed');
|
|
363
|
+
this.error(`Frontend build failed:${detail || ` ${error.message}`}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Copy built UI to backend/dist/ui so the backend can serve it
|
|
367
|
+
await this.copyUIToBackend();
|
|
368
|
+
}
|
|
369
|
+
async copyUIToBackend() {
|
|
370
|
+
const frontendDistPath = node_path_1.default.join(process.cwd(), 'frontend', 'dist');
|
|
371
|
+
const backendPath = (0, checkFramework_js_1.getBackendPath)();
|
|
372
|
+
const backendUiPath = node_path_1.default.join(backendPath, 'dist', 'ui');
|
|
373
|
+
// Check if frontend/dist exists
|
|
374
|
+
if (!(await fs_extra_1.default.pathExists(frontendDistPath))) {
|
|
375
|
+
this.error('Frontend build not found.\n\n' +
|
|
376
|
+
'Expected at: ' +
|
|
377
|
+
frontendDistPath +
|
|
378
|
+
'\n\n' +
|
|
379
|
+
'The frontend build failed or was not created.');
|
|
380
|
+
}
|
|
381
|
+
this.vlog('š Copying UI to backend/dist/ui...');
|
|
244
382
|
try {
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
383
|
+
// Remove old UI if exists
|
|
384
|
+
await fs_extra_1.default.remove(backendUiPath);
|
|
385
|
+
// Create backend/dist directory if it doesn't exist
|
|
386
|
+
await fs_extra_1.default.ensureDir(node_path_1.default.join(backendPath, 'dist'));
|
|
387
|
+
// Copy frontend/dist to backend/dist/ui
|
|
388
|
+
await fs_extra_1.default.copy(frontendDistPath, backendUiPath);
|
|
389
|
+
this.vlog('ā
UI copied to backend successfully');
|
|
249
390
|
}
|
|
250
391
|
catch (error) {
|
|
251
|
-
this.
|
|
252
|
-
this.warn('Continuing anyway, but actions might not be registered correctly.');
|
|
392
|
+
this.error(`Failed to copy UI to backend: ${error.message}`);
|
|
253
393
|
}
|
|
254
394
|
}
|
|
255
395
|
async generateCode() {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
396
|
+
const cwd = process.cwd();
|
|
397
|
+
const backendPath = (0, checkFramework_js_1.getBackendPath)();
|
|
398
|
+
// 1. Backend TypeScript compilation
|
|
399
|
+
const compileOutputFile = 'backend/dist/src/App.js';
|
|
400
|
+
const compileUpToDate = await (0, buildCache_js_1.isUpToDate)({
|
|
401
|
+
cwd,
|
|
402
|
+
sourceGlobs: ['backend/src/**/*.ts', 'backend/package-lock.json'],
|
|
403
|
+
outputFiles: [compileOutputFile],
|
|
404
|
+
});
|
|
405
|
+
if (compileUpToDate) {
|
|
406
|
+
this.vlog('ā
Backend compilation up-to-date, skipping');
|
|
407
|
+
this.recordStep('Backend compilation', 'skipped', 'Up-to-date');
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
this.vlog('Compiling TypeScript code...');
|
|
411
|
+
try {
|
|
412
|
+
// Clean TypeScript output directories to remove stale compiled files from deleted models.
|
|
413
|
+
// Only remove src/ and generated/ (not dist/ui/ which holds the copied frontend build).
|
|
414
|
+
await fs_extra_1.default.remove(node_path_1.default.join(backendPath, 'dist', 'src'));
|
|
415
|
+
await fs_extra_1.default.remove(node_path_1.default.join(backendPath, 'dist', 'generated'));
|
|
416
|
+
if (this.verbose) {
|
|
417
|
+
// Stream build output directly to the terminal in verbose mode to avoid buffering.
|
|
418
|
+
(0, node_child_process_1.execSync)('npm run build', { stdio: 'inherit', cwd: backendPath });
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// Capture output in non-verbose mode so it can be inspected on failure without spamming the console.
|
|
422
|
+
(0, node_child_process_1.execSync)('npm run build', { stdio: 'pipe', cwd: backendPath });
|
|
423
|
+
}
|
|
424
|
+
this.recordStep('Backend compilation', 'success');
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
const detail = this.extractBuildOutput(error);
|
|
428
|
+
this.recordStep('Backend compilation', 'error', 'Compilation failed');
|
|
429
|
+
this.error(`TypeScript compilation failed:${detail || ` ${error.message}`}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
// 2. GraphQL schema generation
|
|
433
|
+
const schemaUpToDate = await (0, buildCache_js_1.isUpToDate)({
|
|
434
|
+
cwd,
|
|
435
|
+
sourceGlobs: [
|
|
436
|
+
'backend/src/data/**/*.ts',
|
|
437
|
+
'backend/src/dataSources/**/*.ts',
|
|
438
|
+
'backend/src/actions/**/*.ts',
|
|
439
|
+
'backend/package-lock.json',
|
|
440
|
+
],
|
|
441
|
+
outputFiles: ['backend/generated/gql/schema.graphql'],
|
|
442
|
+
});
|
|
443
|
+
// 3. GraphQL SDK generation (depends on schema)
|
|
444
|
+
// Evaluate before schema runs; if schema regenerates, SDK must too (see sdkShouldRun below)
|
|
445
|
+
const sdkUpToDate = await (0, buildCache_js_1.isUpToDate)({
|
|
446
|
+
cwd,
|
|
447
|
+
sourceGlobs: ['backend/generated/gql/schema.graphql'],
|
|
448
|
+
outputFiles: ['frontend/generated/gql/types.ts'],
|
|
449
|
+
});
|
|
450
|
+
try {
|
|
451
|
+
if (!schemaUpToDate) {
|
|
452
|
+
this.vlog('Generating GraphQL schema and SDK...');
|
|
453
|
+
await this.config.runCommand('gql', ['generate-schema']);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
this.vlog('ā
GraphQL schema up-to-date, skipping');
|
|
457
|
+
}
|
|
458
|
+
// Run SDK if schema was regenerated OR SDK output was stale
|
|
459
|
+
const sdkShouldRun = !schemaUpToDate || !sdkUpToDate;
|
|
460
|
+
if (sdkShouldRun) {
|
|
461
|
+
if (schemaUpToDate) {
|
|
462
|
+
this.vlog('Generating GraphQL SDK...');
|
|
463
|
+
}
|
|
464
|
+
await this.config.runCommand('gql', ['generate-sdk']);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
this.vlog('ā
GraphQL SDK up-to-date, skipping');
|
|
468
|
+
}
|
|
469
|
+
if (schemaUpToDate && !sdkShouldRun) {
|
|
470
|
+
this.vlog('ā
GraphQL schema and SDK up-to-date');
|
|
471
|
+
this.recordStep('GraphQL schema & SDK', 'skipped', 'Up-to-date');
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
this.vlog('ā
GraphQL schema and SDK generated');
|
|
475
|
+
this.recordStep('GraphQL schema & SDK', 'success');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
this.warn(`Warning: Failed to generate GraphQL artifacts: ${error.message}`);
|
|
480
|
+
this.warn('You can manually generate them with "slingr gql generate-schema" and "slingr gql generate-sdk"');
|
|
481
|
+
this.recordStep('GraphQL schema & SDK', 'warning', error.message);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async runDevMode(flags) {
|
|
485
|
+
this.log('š Starting in development mode (Backend hot-reload + UI dev server)...');
|
|
486
|
+
await this.buildFramework(true);
|
|
487
|
+
await (0, viewsGenerator_js_1.generateViewsContext)(process.cwd(), this.verbose);
|
|
488
|
+
await this.generateCode();
|
|
489
|
+
if (!flags['skip-infra']) {
|
|
490
|
+
await this.checkInfrastructure();
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
this.recordStep('Infrastructure', 'skipped', '--skip-infra');
|
|
494
|
+
}
|
|
495
|
+
const backendPort = flags.port || 3000;
|
|
496
|
+
const uiPort = flags.port || 8000;
|
|
497
|
+
// Check for frontend directory
|
|
498
|
+
const frontendPath = node_path_1.default.join(process.cwd(), 'frontend');
|
|
499
|
+
if (!(await fs_extra_1.default.pathExists(node_path_1.default.join(frontendPath, 'package.json')))) {
|
|
500
|
+
this.error('Frontend directory not found. Cannot run in development mode.\n\n' +
|
|
501
|
+
'Expected frontend directory at: ' +
|
|
502
|
+
frontendPath);
|
|
503
|
+
}
|
|
504
|
+
// Frontend TypeScript type-check (dev servers use transpile-only, so errors don't surface otherwise)
|
|
505
|
+
await this.typeCheckFrontend(frontendPath);
|
|
506
|
+
this.log('ā¹ļø Starting development servers:');
|
|
507
|
+
this.log(` ⢠Backend with hot-reload on port ${backendPort}`);
|
|
508
|
+
this.log(` ⢠UI dev server on port ${uiPort}`);
|
|
509
|
+
this.log('');
|
|
510
|
+
const backendPath = (0, checkFramework_js_1.getBackendPath)();
|
|
511
|
+
const env = {
|
|
512
|
+
...process.env,
|
|
513
|
+
PORT: backendPort.toString(),
|
|
514
|
+
SLINGR_SKIP_UI: 'true', // Skip UI serving in dev mode - frontend runs separately
|
|
515
|
+
LOG_LEVEL: this.verbose ? 'debug' : process.env.LOG_LEVEL || 'info',
|
|
516
|
+
};
|
|
517
|
+
// Filter out noisy UmiJS startup messages that aren't actionable
|
|
518
|
+
const umiNoise = /ä½ ē„éå|Using mako@|Mako is an extremely fast/;
|
|
519
|
+
const forwardStream = (src, dest) => {
|
|
520
|
+
src.on('data', (chunk) => {
|
|
521
|
+
const text = chunk.toString();
|
|
522
|
+
const filtered = text
|
|
523
|
+
.split('\n')
|
|
524
|
+
.filter(line => !umiNoise.test(line))
|
|
525
|
+
.join('\n');
|
|
526
|
+
if (filtered.trim() || text.endsWith('\n')) {
|
|
527
|
+
dest.write(filtered.endsWith('\n') || !filtered ? filtered : filtered + '\n');
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
};
|
|
531
|
+
// Start Backend Process
|
|
532
|
+
this.vlog('š§ Starting backend server...');
|
|
533
|
+
const backendArgs = ['ts-node', 'src/App.ts'];
|
|
534
|
+
const backendCmd = 'npx';
|
|
535
|
+
const backendProcess = (0, node_child_process_1.spawn)(backendCmd, backendArgs, {
|
|
536
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
537
|
+
cwd: backendPath,
|
|
538
|
+
env,
|
|
539
|
+
shell: true,
|
|
540
|
+
});
|
|
541
|
+
forwardStream(backendProcess.stdout, process.stdout);
|
|
542
|
+
forwardStream(backendProcess.stderr, process.stderr);
|
|
543
|
+
// Start UI Process
|
|
544
|
+
this.vlog('šØ Starting UI development server...');
|
|
545
|
+
const uiProcess = (0, node_child_process_1.spawn)('npm', ['run', 'dev'], {
|
|
546
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
547
|
+
cwd: frontendPath,
|
|
548
|
+
env: { ...process.env, PORT: uiPort.toString() },
|
|
549
|
+
shell: true,
|
|
550
|
+
});
|
|
551
|
+
forwardStream(uiProcess.stdout, process.stdout);
|
|
552
|
+
forwardStream(uiProcess.stderr, process.stderr);
|
|
553
|
+
// Wait for both servers to signal ready, then show the pre-flight summary at the bottom
|
|
554
|
+
await Promise.all([
|
|
555
|
+
this.waitForReady(backendProcess, /Slingr application is ready/, 15000),
|
|
556
|
+
this.waitForReady(uiProcess, /App listening at/, 30000),
|
|
557
|
+
]);
|
|
558
|
+
this.printSummary();
|
|
559
|
+
// Graceful Shutdown
|
|
560
|
+
let isShuttingDown = false;
|
|
561
|
+
const cleanup = async () => {
|
|
562
|
+
if (isShuttingDown) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
isShuttingDown = true;
|
|
566
|
+
this.log('\nš Shutting down development servers...');
|
|
567
|
+
try {
|
|
568
|
+
if (backendProcess) {
|
|
569
|
+
if (process.platform === 'win32' && backendProcess.pid) {
|
|
570
|
+
(0, node_child_process_1.execSync)(`taskkill /F /T /PID ${backendProcess.pid}`, { stdio: 'ignore' });
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
backendProcess.kill('SIGTERM');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (uiProcess) {
|
|
577
|
+
if (process.platform === 'win32' && uiProcess.pid) {
|
|
578
|
+
(0, node_child_process_1.execSync)(`taskkill /F /T /PID ${uiProcess.pid}`, { stdio: 'ignore' });
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
uiProcess.kill('SIGTERM');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
catch { }
|
|
586
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
587
|
+
process.exit(0);
|
|
588
|
+
};
|
|
589
|
+
process.on('SIGINT', cleanup);
|
|
590
|
+
process.on('SIGTERM', cleanup);
|
|
591
|
+
process.on('exit', cleanup);
|
|
592
|
+
// Keep process alive to listen for signals
|
|
593
|
+
await new Promise(() => { });
|
|
594
|
+
}
|
|
595
|
+
/** Run tsc --noEmit in the frontend directory to surface TypeScript errors before the dev server starts. */
|
|
596
|
+
async typeCheckFrontend(frontendPath) {
|
|
597
|
+
// Skip if no tsconfig.json ā project may not use TypeScript
|
|
598
|
+
const tsconfig = node_path_1.default.join(frontendPath, 'tsconfig.json');
|
|
599
|
+
if (!(await fs_extra_1.default.pathExists(tsconfig))) {
|
|
600
|
+
this.recordStep('Frontend type check', 'skipped', 'no tsconfig.json');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
// Prefer a non-interactive local invocation of tsc to avoid npx network installs/prompts
|
|
604
|
+
const localTsc = node_path_1.default.join(frontendPath, 'node_modules', '.bin', process.platform === 'win32' ? 'tsc.cmd' : 'tsc');
|
|
605
|
+
if (!(await fs_extra_1.default.pathExists(localTsc))) {
|
|
606
|
+
this.recordStep('Frontend type check', 'skipped', 'TypeScript tooling not found in frontend node_modules');
|
|
607
|
+
this.warn('Skipping frontend type check: local TypeScript compiler not found (install dev dependencies to enable this check).');
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
this.vlog('š Type-checking frontend...');
|
|
611
|
+
try {
|
|
612
|
+
const output = (0, node_child_process_1.execSync)(`"${localTsc}" --noEmit`, { stdio: 'pipe', cwd: frontendPath });
|
|
613
|
+
if (this.verbose && output.toString().trim()) {
|
|
614
|
+
process.stdout.write(output.toString());
|
|
615
|
+
}
|
|
616
|
+
this.recordStep('Frontend type check', 'success');
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
const detail = this.extractBuildOutput(error);
|
|
620
|
+
this.recordStep('Frontend type check', 'warning', 'TypeScript errors detected');
|
|
621
|
+
// Show errors but continue ā dev server still runs with transpile-only mode
|
|
622
|
+
if (detail) {
|
|
623
|
+
this.warn(`Frontend TypeScript errors:${detail}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/** Resolves when the process emits output matching `pattern`, or after `timeoutMs` as a fallback. */
|
|
628
|
+
waitForReady(proc, pattern, timeoutMs) {
|
|
629
|
+
return new Promise((resolve, reject) => {
|
|
630
|
+
let done = false;
|
|
631
|
+
let timeout;
|
|
632
|
+
const cleanup = () => {
|
|
633
|
+
if (timeout) {
|
|
634
|
+
clearTimeout(timeout);
|
|
635
|
+
timeout = undefined;
|
|
636
|
+
}
|
|
637
|
+
proc.stdout?.off('data', onData);
|
|
638
|
+
proc.stderr?.off('data', onData);
|
|
639
|
+
proc.off('exit', onExit);
|
|
640
|
+
proc.off('close', onClose);
|
|
641
|
+
proc.off('error', onError);
|
|
642
|
+
};
|
|
643
|
+
const finishResolve = () => {
|
|
644
|
+
if (done)
|
|
645
|
+
return;
|
|
646
|
+
done = true;
|
|
647
|
+
cleanup();
|
|
648
|
+
resolve();
|
|
649
|
+
};
|
|
650
|
+
const finishReject = (error) => {
|
|
651
|
+
if (done)
|
|
652
|
+
return;
|
|
653
|
+
done = true;
|
|
654
|
+
cleanup();
|
|
655
|
+
reject(error);
|
|
656
|
+
};
|
|
657
|
+
const onData = (chunk) => {
|
|
658
|
+
if (pattern.test(chunk.toString())) {
|
|
659
|
+
finishResolve();
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
const onExit = (code) => {
|
|
663
|
+
if (!done) {
|
|
664
|
+
finishReject(new Error(`Process exited before matching readiness pattern (code ${code ?? 'null'})`));
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
const onClose = (code) => {
|
|
668
|
+
if (!done) {
|
|
669
|
+
finishReject(new Error(`Process closed before matching readiness pattern (code ${code ?? 'null'})`));
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
const onError = (error) => {
|
|
673
|
+
if (!done) {
|
|
674
|
+
finishReject(error);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
proc.stdout?.on('data', onData);
|
|
678
|
+
proc.stderr?.on('data', onData);
|
|
679
|
+
proc.on('exit', onExit);
|
|
680
|
+
proc.on('close', onClose);
|
|
681
|
+
proc.on('error', onError);
|
|
682
|
+
timeout = setTimeout(() => {
|
|
683
|
+
// Preserve previous behavior: resolve after timeout as a fallback.
|
|
684
|
+
finishResolve();
|
|
685
|
+
}, timeoutMs);
|
|
686
|
+
});
|
|
259
687
|
}
|
|
260
688
|
async loadDataSources() {
|
|
261
689
|
const dataSources = [];
|
|
262
|
-
const
|
|
690
|
+
const backendPath = (0, checkFramework_js_1.getBackendPath)();
|
|
691
|
+
const dataSourcesPath = node_path_1.default.join(backendPath, 'src', 'dataSources');
|
|
263
692
|
if (await fs_extra_1.default.pathExists(dataSourcesPath)) {
|
|
264
693
|
const files = await fs_extra_1.default.readdir(dataSourcesPath);
|
|
265
694
|
for (const file of files) {
|
|
@@ -281,32 +710,101 @@ class Run extends core_1.Command {
|
|
|
281
710
|
}
|
|
282
711
|
return dataSources;
|
|
283
712
|
}
|
|
284
|
-
async
|
|
285
|
-
|
|
286
|
-
//
|
|
713
|
+
async runUIOnly(port) {
|
|
714
|
+
const uiPort = port || 8000;
|
|
715
|
+
// Check for frontend directory
|
|
716
|
+
const frontendPath = node_path_1.default.join(process.cwd(), 'frontend');
|
|
717
|
+
if (!(await fs_extra_1.default.pathExists(node_path_1.default.join(frontendPath, 'package.json')))) {
|
|
718
|
+
this.error('Frontend directory not found. Cannot run UI-only mode.\n\n' + 'Expected frontend directory at: ' + frontendPath);
|
|
719
|
+
}
|
|
720
|
+
await (0, viewsGenerator_js_1.generateViewsContext)(process.cwd(), this.verbose);
|
|
721
|
+
this.log('šØ Starting UI development server...');
|
|
722
|
+
this.log(`Starting UI dev server on port ${uiPort}...`);
|
|
723
|
+
this.log('');
|
|
287
724
|
try {
|
|
288
|
-
this.log('');
|
|
289
725
|
(0, node_child_process_1.execSync)('npm run dev', {
|
|
290
726
|
stdio: 'inherit',
|
|
291
|
-
cwd:
|
|
727
|
+
cwd: frontendPath,
|
|
728
|
+
env: { ...process.env, PORT: uiPort.toString() },
|
|
292
729
|
});
|
|
293
730
|
}
|
|
294
731
|
catch (error) {
|
|
295
|
-
// If it's Ctrl+C, exit gracefully
|
|
296
732
|
if (error.signal === 'SIGINT') {
|
|
297
|
-
this.log('\nš
|
|
733
|
+
this.log('\nš UI server stopped gracefully');
|
|
298
734
|
process.exit(0);
|
|
299
735
|
}
|
|
300
|
-
|
|
301
|
-
|
|
736
|
+
this.error(`Failed to start UI server: ${error.message}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async runBackendOnly(flags) {
|
|
740
|
+
this.vlog('āļø Starting backend server only...');
|
|
741
|
+
await this.buildFramework();
|
|
742
|
+
await this.generateCode();
|
|
743
|
+
if (!flags['skip-infra']) {
|
|
744
|
+
await this.checkInfrastructure();
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
this.recordStep('Infrastructure', 'skipped', '--skip-infra');
|
|
748
|
+
}
|
|
749
|
+
this.printSummary();
|
|
750
|
+
await this.executeApp(flags.port);
|
|
751
|
+
}
|
|
752
|
+
async executeApp(port, extraEnv = {}, production = false) {
|
|
753
|
+
this.vlog('š Starting Slingr application...');
|
|
754
|
+
const backendPath = (0, checkFramework_js_1.getBackendPath)();
|
|
755
|
+
const env = { ...process.env, ...extraEnv };
|
|
756
|
+
env.LOG_LEVEL = this.verbose ? 'debug' : process.env.LOG_LEVEL || 'info';
|
|
757
|
+
if (port) {
|
|
758
|
+
env.PORT = port.toString();
|
|
759
|
+
this.vlog(`Using port: ${port}`);
|
|
760
|
+
}
|
|
761
|
+
if (production) {
|
|
762
|
+
// Production mode: run compiled version
|
|
302
763
|
try {
|
|
303
|
-
|
|
764
|
+
this.vlog('');
|
|
765
|
+
const entryJs = 'dist/src/App.js';
|
|
766
|
+
const entryJsPath = node_path_1.default.join(backendPath, entryJs);
|
|
767
|
+
if (!(await fs_extra_1.default.pathExists(entryJsPath))) {
|
|
768
|
+
this.error('Compiled backend not found at: ' +
|
|
769
|
+
entryJsPath +
|
|
770
|
+
'\n\n' +
|
|
771
|
+
'Please ensure the backend was compiled successfully.');
|
|
772
|
+
}
|
|
773
|
+
(0, node_child_process_1.execSync)(`node ${entryJs}`, {
|
|
304
774
|
stdio: 'inherit',
|
|
305
|
-
cwd:
|
|
775
|
+
cwd: backendPath,
|
|
776
|
+
env,
|
|
306
777
|
});
|
|
307
778
|
}
|
|
308
|
-
catch (
|
|
309
|
-
|
|
779
|
+
catch (error) {
|
|
780
|
+
// If it's Ctrl+C, exit gracefully
|
|
781
|
+
if (error.signal === 'SIGINT') {
|
|
782
|
+
this.log('\nš Application stopped gracefully');
|
|
783
|
+
process.exit(0);
|
|
784
|
+
}
|
|
785
|
+
this.error(`Failed to start application: ${error.message}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
// Development mode: use ts-node src/App.ts or npm run dev
|
|
790
|
+
try {
|
|
791
|
+
this.vlog('');
|
|
792
|
+
const devCmd = 'npx ts-node src/App.ts';
|
|
793
|
+
this.vlog(`Running: ${devCmd} (cwd: ${backendPath})`);
|
|
794
|
+
(0, node_child_process_1.execSync)(devCmd, {
|
|
795
|
+
stdio: 'inherit',
|
|
796
|
+
cwd: backendPath,
|
|
797
|
+
env,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
// If it's Ctrl+C, exit gracefully
|
|
802
|
+
if (error.signal === 'SIGINT') {
|
|
803
|
+
this.log('\nš Application stopped gracefully');
|
|
804
|
+
process.exit(0);
|
|
805
|
+
}
|
|
806
|
+
// In dev mode, fail if startup fails - don't fall back to production
|
|
807
|
+
this.error(`Failed to start application in development mode: ${error.message}`);
|
|
310
808
|
}
|
|
311
809
|
}
|
|
312
810
|
}
|