cloud-ide-cide 2.0.34
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/buildAllProjects.js +131 -0
- package/buildProject.js +209 -0
- package/buildWorkspace.js +225 -0
- package/cideShell.js +1292 -0
- package/cli.js +521 -0
- package/createProject.js +71 -0
- package/deployer/node/upload-api.js +265 -0
- package/deployer/php/setup.php +332 -0
- package/deployer/php/upload-ui.php +294 -0
- package/package.json +53 -0
- package/publishPackage.js +969 -0
- package/resolveNgProjectName.js +94 -0
- package/serverInit.js +665 -0
- package/startProject.js +57 -0
- package/uploadProject.js +727 -0
- package/watchLinkProject.js +40 -0
package/cideShell.js
ADDED
|
@@ -0,0 +1,1292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cide shell — interactive workspace REPL (build, publish, git, server, npm, env).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const { execSync, spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const buildProject = require('./buildProject');
|
|
11
|
+
const buildWorkspace = require('./buildWorkspace');
|
|
12
|
+
const buildAllProjects = require('./buildAllProjects');
|
|
13
|
+
const startProject = require('./startProject');
|
|
14
|
+
const watchLinkProject = require('./watchLinkProject');
|
|
15
|
+
const publishPackage = require('./publishPackage');
|
|
16
|
+
const uploadProject = require('./uploadProject');
|
|
17
|
+
const serverInit = require('./serverInit');
|
|
18
|
+
|
|
19
|
+
const loadCideConfig = publishPackage.loadCideConfig;
|
|
20
|
+
const discoverPublishablePackages = publishPackage.discoverPublishablePackages;
|
|
21
|
+
const installGlobalPackage = publishPackage.installGlobalPackage;
|
|
22
|
+
|
|
23
|
+
const PROMPT = 'CloudIDE> ';
|
|
24
|
+
|
|
25
|
+
/** Shell: `build -p all` / `build --resume-from 2` → options for buildWorkspace */
|
|
26
|
+
function parseBuildShellArgs(tokens) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
29
|
+
const t = String(tokens[i]);
|
|
30
|
+
if (t === '-p' || t === '--packages') {
|
|
31
|
+
out.packages = tokens[++i];
|
|
32
|
+
} else if (t === '--resume-from') {
|
|
33
|
+
out.resumeFrom = tokens[++i];
|
|
34
|
+
} else if (t === '--continue-on-error') {
|
|
35
|
+
out.continueOnError = true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ENV_KEYS_TO_CHECK = [
|
|
42
|
+
'MONGODB_URI',
|
|
43
|
+
'JWT_SECRET',
|
|
44
|
+
'PORT',
|
|
45
|
+
'SECRET_KEY',
|
|
46
|
+
'EMAIL_GATEWAY_URL',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
let bgServerPid = null;
|
|
50
|
+
|
|
51
|
+
function existsSync(p) {
|
|
52
|
+
try {
|
|
53
|
+
fs.accessSync(p);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readJson(p) {
|
|
61
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function safeReadJson(p) {
|
|
65
|
+
try {
|
|
66
|
+
return readJson(p);
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function safeReaddir(dir) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
75
|
+
} catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** True if dir contains cide.json marking an Angular workspace (supports templete/template typo). */
|
|
81
|
+
function isAngularCideJsonAt(dir) {
|
|
82
|
+
const cidePath = path.join(dir, 'cide.json');
|
|
83
|
+
if (!existsSync(cidePath)) return false;
|
|
84
|
+
const c = safeReadJson(cidePath);
|
|
85
|
+
return !!(c && (c.templete === 'angular' || c.template === 'angular'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find Angular workspace root (folder with angular cide.json). No hardcoded folder names.
|
|
90
|
+
*
|
|
91
|
+
* 1) Walk up from cwd until a matching cide.json is found.
|
|
92
|
+
* 2) For each ancestor, scan that ancestor’s immediate subfolders (siblings of your current path) for angular cide.json — supports running from monorepo root or from a sibling package (e.g. CLI folder next to the Angular app).
|
|
93
|
+
* 3) Shallow BFS under cwd (max depth), skipping node_modules / dist — supports nested layouts.
|
|
94
|
+
*/
|
|
95
|
+
function findAngularWorkspaceRoot() {
|
|
96
|
+
const cwd = path.resolve(process.cwd());
|
|
97
|
+
|
|
98
|
+
let d = cwd;
|
|
99
|
+
for (let i = 0; i < 16; i++) {
|
|
100
|
+
if (isAngularCideJsonAt(d)) return d;
|
|
101
|
+
const parent = path.dirname(d);
|
|
102
|
+
if (parent === d) break;
|
|
103
|
+
d = parent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
d = cwd;
|
|
107
|
+
for (let i = 0; i < 16; i++) {
|
|
108
|
+
const parent = path.dirname(d);
|
|
109
|
+
if (parent === d) break;
|
|
110
|
+
for (const e of safeReaddir(parent)) {
|
|
111
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
112
|
+
const sibling = path.resolve(parent, e.name);
|
|
113
|
+
if (isAngularCideJsonAt(sibling)) return sibling;
|
|
114
|
+
}
|
|
115
|
+
d = parent;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const maxDepth = 5;
|
|
119
|
+
const queue = [[cwd, 0]];
|
|
120
|
+
const seen = new Set([cwd]);
|
|
121
|
+
while (queue.length > 0) {
|
|
122
|
+
const [dir, depth] = queue.shift();
|
|
123
|
+
if (isAngularCideJsonAt(dir)) return dir;
|
|
124
|
+
if (depth >= maxDepth) continue;
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
for (const e of entries) {
|
|
132
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.')) continue;
|
|
133
|
+
const full = path.resolve(dir, e.name);
|
|
134
|
+
if (seen.has(full)) continue;
|
|
135
|
+
seen.add(full);
|
|
136
|
+
queue.push([full, depth + 1]);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function discoverGitRepos(monorepoRoot) {
|
|
144
|
+
const repos = [];
|
|
145
|
+
if (!existsSync(monorepoRoot)) return repos;
|
|
146
|
+
for (const e of safeReaddir(monorepoRoot)) {
|
|
147
|
+
if (!e.isDirectory() || e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
148
|
+
const abs = path.join(monorepoRoot, e.name);
|
|
149
|
+
if (existsSync(path.join(abs, '.git'))) {
|
|
150
|
+
repos.push({ name: e.name, absPath: abs });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
repos.sort((a, b) => a.name.localeCompare(b.name));
|
|
154
|
+
return repos;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Every folder under projects/ (and projects/@cloudidesys/) that has ng-package.json */
|
|
158
|
+
function discoverAngularLibs(angularRoot) {
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
const out = [];
|
|
161
|
+
function pushLib(projDir, folderName) {
|
|
162
|
+
const resolved = path.resolve(projDir);
|
|
163
|
+
if (seen.has(resolved)) return;
|
|
164
|
+
const ngp = path.join(projDir, 'ng-package.json');
|
|
165
|
+
if (!existsSync(ngp)) return;
|
|
166
|
+
seen.add(resolved);
|
|
167
|
+
const pjPath = path.join(projDir, 'package.json');
|
|
168
|
+
const pkg = safeReadJson(pjPath) || {};
|
|
169
|
+
const ng = safeReadJson(ngp) || {};
|
|
170
|
+
let distDir = null;
|
|
171
|
+
if (ng.dest) distDir = path.resolve(projDir, ng.dest);
|
|
172
|
+
const short = folderName.startsWith('cloud-ide-') ? folderName.slice('cloud-ide-'.length) : folderName;
|
|
173
|
+
out.push({
|
|
174
|
+
folderName,
|
|
175
|
+
shortName: short,
|
|
176
|
+
packageName: pkg.name || folderName,
|
|
177
|
+
absDir: resolved,
|
|
178
|
+
distDir,
|
|
179
|
+
version: pkg.version || '0.0.0',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const projectsDir = path.join(angularRoot, 'projects');
|
|
183
|
+
if (!existsSync(projectsDir)) return out;
|
|
184
|
+
for (const e of safeReaddir(projectsDir)) {
|
|
185
|
+
if (!e.isDirectory()) continue;
|
|
186
|
+
if (e.name === '@cloudidesys') {
|
|
187
|
+
const scoped = path.join(projectsDir, e.name);
|
|
188
|
+
for (const e2 of safeReaddir(scoped)) {
|
|
189
|
+
if (!e2.isDirectory()) continue;
|
|
190
|
+
pushLib(path.join(scoped, e2.name), e2.name);
|
|
191
|
+
}
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
pushLib(path.join(projectsDir, e.name), e.name);
|
|
195
|
+
}
|
|
196
|
+
out.sort((a, b) => a.folderName.localeCompare(b.folderName));
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function discoverSeedScripts(nodeRoot) {
|
|
201
|
+
const dir = path.join(nodeRoot, 'src', 'scripts');
|
|
202
|
+
const out = [];
|
|
203
|
+
if (!existsSync(dir)) return out;
|
|
204
|
+
for (const f of fs.readdirSync(dir)) {
|
|
205
|
+
if (!f.endsWith('.ts')) continue;
|
|
206
|
+
const base = f.replace(/\.ts$/, '');
|
|
207
|
+
out.push({ name: base, file: f, absPath: path.join(dir, f) });
|
|
208
|
+
}
|
|
209
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function discoverNodeBackend(monorepoRoot) {
|
|
214
|
+
if (!existsSync(monorepoRoot)) return null;
|
|
215
|
+
for (const e of safeReaddir(monorepoRoot)) {
|
|
216
|
+
if (!e.isDirectory()) continue;
|
|
217
|
+
const abs = path.join(monorepoRoot, e.name);
|
|
218
|
+
const cidePath = path.join(abs, 'cide.json');
|
|
219
|
+
if (!existsSync(cidePath)) continue;
|
|
220
|
+
const c = safeReadJson(cidePath);
|
|
221
|
+
if (c && c.templete === 'node') {
|
|
222
|
+
const pj = path.join(abs, 'package.json');
|
|
223
|
+
const pkg = safeReadJson(pj) || {};
|
|
224
|
+
return { name: e.name, absPath: abs, scripts: pkg.scripts || {} };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Projects that can be started via `npm run <script>` (shown in `start` picker).
|
|
232
|
+
*/
|
|
233
|
+
function discoverRunnableProjects(ws) {
|
|
234
|
+
const list = [];
|
|
235
|
+
if (ws.angularRoot) {
|
|
236
|
+
const pj = path.join(ws.angularRoot, 'package.json');
|
|
237
|
+
const pkg = safeReadJson(pj) || {};
|
|
238
|
+
const scripts = pkg.scripts || {};
|
|
239
|
+
const angularShortName = path.basename(ws.angularRoot);
|
|
240
|
+
if (scripts.start) {
|
|
241
|
+
list.push({
|
|
242
|
+
kind: 'angular',
|
|
243
|
+
label: `${angularShortName} — Angular app (npm run start → ng serve)`,
|
|
244
|
+
cwd: ws.angularRoot,
|
|
245
|
+
npmScript: 'start',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (scripts.tailwindcss) {
|
|
249
|
+
list.push({
|
|
250
|
+
kind: 'tailwind',
|
|
251
|
+
label: `${angularShortName} — Tailwind CSS watch`,
|
|
252
|
+
cwd: ws.angularRoot,
|
|
253
|
+
npmScript: 'tailwindcss',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (ws.nodeBackend) {
|
|
258
|
+
const scripts = ws.nodeBackend.scripts || {};
|
|
259
|
+
const scriptName = scripts.dev ? 'dev' : scripts.start ? 'start' : null;
|
|
260
|
+
if (scriptName) {
|
|
261
|
+
list.push({
|
|
262
|
+
kind: 'node',
|
|
263
|
+
label: `${ws.nodeBackend.name} — Node API (npm run ${scriptName})`,
|
|
264
|
+
cwd: ws.nodeBackend.absPath,
|
|
265
|
+
npmScript: scriptName,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
list.forEach((e, i) => {
|
|
270
|
+
e.index = i + 1;
|
|
271
|
+
});
|
|
272
|
+
return list;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseNumberSelection(raw, maxN) {
|
|
276
|
+
const s = String(raw).trim().toLowerCase();
|
|
277
|
+
if (s === 'q' || s === 'quit') return { ok: true, indices: [], cancelled: true };
|
|
278
|
+
if (!s) return { ok: false, error: 'Empty selection.' };
|
|
279
|
+
if (s === 'all') {
|
|
280
|
+
return { ok: true, indices: Array.from({ length: maxN }, (_, i) => i) };
|
|
281
|
+
}
|
|
282
|
+
const set = new Set();
|
|
283
|
+
const parts = s.split(',').map((p) => p.trim()).filter(Boolean);
|
|
284
|
+
for (const part of parts) {
|
|
285
|
+
if (part.includes('-')) {
|
|
286
|
+
const [a, b] = part.split('-').map((x) => parseInt(x.trim(), 10));
|
|
287
|
+
if (Number.isFinite(a) && Number.isFinite(b)) {
|
|
288
|
+
const lo = Math.min(a, b);
|
|
289
|
+
const hi = Math.max(a, b);
|
|
290
|
+
for (let i = lo; i <= hi; i++) {
|
|
291
|
+
if (i >= 1 && i <= maxN) set.add(i - 1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
const n = parseInt(part, 10);
|
|
296
|
+
if (n >= 1 && n <= maxN) set.add(n - 1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const indices = [...set].sort((a, b) => a - b);
|
|
300
|
+
if (indices.length === 0) {
|
|
301
|
+
return { ok: false, error: `No valid numbers. Use 1–${maxN}, ranges like 1-3, or all.` };
|
|
302
|
+
}
|
|
303
|
+
return { ok: true, indices };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** True when stdin is likely VS Code / Cursor integrated terminal (not a standalone OS terminal). */
|
|
307
|
+
function isLikelyIdeIntegratedTerminal() {
|
|
308
|
+
return (
|
|
309
|
+
process.env.TERM_PROGRAM === 'vscode' ||
|
|
310
|
+
process.env.VSCODE_INJECTION === '1' ||
|
|
311
|
+
process.env.VSCODE_CWD !== undefined ||
|
|
312
|
+
process.env.CURSOR_TRACE_ID !== undefined
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Commands to paste into a new integrated terminal (preferred when running inside Cursor/VS Code). */
|
|
317
|
+
function printIntegratedTerminalInstructions(cwd, script) {
|
|
318
|
+
const cmdLine =
|
|
319
|
+
process.platform === 'win32'
|
|
320
|
+
? `cd /d "${cwd}" && npm run ${script}`
|
|
321
|
+
: `cd ${JSON.stringify(cwd)} && npm run ${script}`;
|
|
322
|
+
console.log('');
|
|
323
|
+
console.log(' Open a new integrated terminal (Terminal → New Terminal, or Ctrl+Shift+`), then paste:');
|
|
324
|
+
console.log(` ${cmdLine}`);
|
|
325
|
+
console.log('');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* When running inside Cursor/VS Code: print paste-ready commands (integrated terminal).
|
|
330
|
+
* Otherwise: open a new OS terminal window (Windows cmd) or print for macOS/Linux.
|
|
331
|
+
*/
|
|
332
|
+
function launchInNewTerminal(entry) {
|
|
333
|
+
const cwd = path.resolve(entry.cwd);
|
|
334
|
+
const script = String(entry.npmScript || '').replace(/[^a-zA-Z0-9:_-]/g, '');
|
|
335
|
+
if (!script) {
|
|
336
|
+
throw new Error('Invalid npm script name');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isLikelyIdeIntegratedTerminal()) {
|
|
340
|
+
printIntegratedTerminalInstructions(cwd, script);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (process.platform === 'win32') {
|
|
345
|
+
// Avoid: start "title" cmd /k JSON.stringify(...) — cmd misparses JSON escapes and shows
|
|
346
|
+
// "The filename, directory name, or volume label syntax is incorrect."
|
|
347
|
+
// Also avoid Unicode/special chars in start's window title (—, →, parentheses, &).
|
|
348
|
+
const runLine = `cd /d "${cwd}" && npm run ${script}`;
|
|
349
|
+
const comspec = process.env.ComSpec || 'cmd.exe';
|
|
350
|
+
const child = spawn(comspec, ['/c', 'start', 'cide', 'cmd', '/k', runLine], {
|
|
351
|
+
detached: true,
|
|
352
|
+
stdio: 'ignore',
|
|
353
|
+
windowsHide: false,
|
|
354
|
+
});
|
|
355
|
+
child.unref();
|
|
356
|
+
} else {
|
|
357
|
+
console.log(`\n Open a new terminal and run:\n cd ${JSON.stringify(cwd)} && npm run ${script}\n`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function questionRl(rl, promptText) {
|
|
362
|
+
return new Promise((resolve) => {
|
|
363
|
+
rl.question(promptText, resolve);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function handleStartCommand(ws, rl, rest) {
|
|
368
|
+
const joined = rest.join(' ').trim();
|
|
369
|
+
if (joined.toLowerCase() === 'legacy') {
|
|
370
|
+
handleStartLegacyProject(ws);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const runnable = discoverRunnableProjects(ws);
|
|
375
|
+
if (!runnable.length) {
|
|
376
|
+
console.log(
|
|
377
|
+
'No runnable projects found (need npm scripts: start in Angular workspace, dev/start in Node API, optional tailwindcss).'
|
|
378
|
+
);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let spec = joined;
|
|
383
|
+
if (!spec) {
|
|
384
|
+
console.log(`
|
|
385
|
+
----------------------------------------------------------------
|
|
386
|
+
START — dev servers (npm run …)
|
|
387
|
+
----------------------------------------------------------------
|
|
388
|
+
In Cursor / VS Code: prints commands to paste in a new integrated terminal (recommended).
|
|
389
|
+
Standalone terminal (outside IDE): opens a new OS window on Windows.
|
|
390
|
+
|
|
391
|
+
`);
|
|
392
|
+
runnable.forEach((r) => {
|
|
393
|
+
console.log(` [${r.index}] ${r.kind.padEnd(10)} ${r.label}`);
|
|
394
|
+
});
|
|
395
|
+
console.log(`
|
|
396
|
+
Pick one line (same rules as publish):
|
|
397
|
+
1 only [1]
|
|
398
|
+
1,3 [1] and [3]
|
|
399
|
+
1-3 [1] through [3]
|
|
400
|
+
all every item above
|
|
401
|
+
q cancel
|
|
402
|
+
|
|
403
|
+
Or re-run non-interactively: start 1 | start 1,3 | start all
|
|
404
|
+
----------------------------------------------------------------
|
|
405
|
+
`);
|
|
406
|
+
spec = (await questionRl(rl, 'Which to run? ')).trim();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!spec || spec.toLowerCase() === 'q') {
|
|
410
|
+
console.log('(cancelled)');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const parsed = parseNumberSelection(spec, runnable.length);
|
|
415
|
+
if (parsed.cancelled) {
|
|
416
|
+
console.log('(cancelled)');
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (!parsed.ok) {
|
|
420
|
+
console.error(parsed.error || 'Invalid selection');
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (!parsed.indices.length) {
|
|
424
|
+
console.log('Nothing selected.');
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const inIde = isLikelyIdeIntegratedTerminal();
|
|
429
|
+
for (const idx of parsed.indices) {
|
|
430
|
+
const entry = runnable[idx];
|
|
431
|
+
if (!entry) continue;
|
|
432
|
+
console.log(
|
|
433
|
+
`\n → ${inIde ? 'Paste in new terminal' : 'New OS window'}: [${entry.index}] ${entry.label}`
|
|
434
|
+
);
|
|
435
|
+
try {
|
|
436
|
+
launchInNewTerminal(entry);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
console.error(` Failed: ${e.message}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
console.log(
|
|
442
|
+
inIde
|
|
443
|
+
? '\nDone. Use Terminal → New Terminal, then paste each command above.\n'
|
|
444
|
+
: '\nDone. Check the new terminal window(s).\n'
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function loadWorkspace() {
|
|
449
|
+
const angularRoot = findAngularWorkspaceRoot();
|
|
450
|
+
if (!angularRoot) {
|
|
451
|
+
return {
|
|
452
|
+
angularRoot: null,
|
|
453
|
+
monorepoRoot: null,
|
|
454
|
+
cide: {},
|
|
455
|
+
repos: [],
|
|
456
|
+
angularLibs: [],
|
|
457
|
+
seedScripts: [],
|
|
458
|
+
nodeBackend: null,
|
|
459
|
+
publishList: [],
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const { cide, monorepoRoot } = loadCideConfig(angularRoot);
|
|
463
|
+
const repos = discoverGitRepos(monorepoRoot);
|
|
464
|
+
const angularLibs = discoverAngularLibs(angularRoot);
|
|
465
|
+
const nodeBackend = discoverNodeBackend(monorepoRoot);
|
|
466
|
+
const seedScripts = nodeBackend ? discoverSeedScripts(nodeBackend.absPath) : [];
|
|
467
|
+
// Lazy publish list: large monorepo scans run on first `packages` / `versions`, not every shell startup.
|
|
468
|
+
const ws = {
|
|
469
|
+
angularRoot,
|
|
470
|
+
monorepoRoot,
|
|
471
|
+
cide,
|
|
472
|
+
repos,
|
|
473
|
+
angularLibs,
|
|
474
|
+
seedScripts,
|
|
475
|
+
nodeBackend,
|
|
476
|
+
_publishListCache: undefined,
|
|
477
|
+
};
|
|
478
|
+
Object.defineProperty(ws, 'publishList', {
|
|
479
|
+
get() {
|
|
480
|
+
if (this._publishListCache !== undefined) return this._publishListCache;
|
|
481
|
+
const { cide: cc, monorepoRoot: mr } = loadCideConfig(this.angularRoot);
|
|
482
|
+
this._publishListCache = discoverPublishablePackages({ cide: cc, angularRoot: this.angularRoot, monorepoRoot: mr });
|
|
483
|
+
return this._publishListCache;
|
|
484
|
+
},
|
|
485
|
+
enumerable: true,
|
|
486
|
+
});
|
|
487
|
+
return ws;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function gitShortStatus(repoPath) {
|
|
491
|
+
try {
|
|
492
|
+
return execSync('git status --porcelain', { cwd: repoPath, encoding: 'utf8', shell: true }).trim();
|
|
493
|
+
} catch {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function gitAheadCount(repoPath) {
|
|
499
|
+
try {
|
|
500
|
+
const o = execSync('git rev-list @{u}..HEAD --count', {
|
|
501
|
+
cwd: repoPath,
|
|
502
|
+
encoding: 'utf8',
|
|
503
|
+
shell: true,
|
|
504
|
+
}).trim();
|
|
505
|
+
const n = parseInt(o, 10);
|
|
506
|
+
return Number.isFinite(n) ? n : 0;
|
|
507
|
+
} catch {
|
|
508
|
+
return 0;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** One git call for startup banner (replaces status + rev-list per repo). */
|
|
513
|
+
function gitRepoStatusSummary(repoPath) {
|
|
514
|
+
try {
|
|
515
|
+
const out = execSync('git status -sb', { cwd: repoPath, encoding: 'utf8', shell: true }).trim();
|
|
516
|
+
if (!out) return { hint: 'clean' };
|
|
517
|
+
const lines = out.split(/\r?\n/);
|
|
518
|
+
const first = lines[0];
|
|
519
|
+
let ahead = 0;
|
|
520
|
+
const am = first.match(/\[ahead (\d+)\]/);
|
|
521
|
+
if (am) ahead = parseInt(am[1], 10);
|
|
522
|
+
const dirtyLines = Math.max(0, lines.length - 1);
|
|
523
|
+
let hint = 'clean';
|
|
524
|
+
if (dirtyLines > 0) hint = `${dirtyLines} local change(s) -> try: commit "message"`;
|
|
525
|
+
else if (ahead > 0) hint = `${ahead} unpushed commit(s) -> try: push`;
|
|
526
|
+
return { hint };
|
|
527
|
+
} catch {
|
|
528
|
+
return { hint: '(git failed)' };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function findRepo(ws, name) {
|
|
533
|
+
if (!name) return null;
|
|
534
|
+
const n = name.toLowerCase();
|
|
535
|
+
return ws.repos.find((r) => r.name.toLowerCase() === n) || null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function resolveBuildToken(token, ws) {
|
|
539
|
+
const t = String(token).trim().toLowerCase();
|
|
540
|
+
if (!t) return null;
|
|
541
|
+
for (const L of ws.angularLibs) {
|
|
542
|
+
if (L.shortName === t) return L.folderName;
|
|
543
|
+
if (L.folderName.toLowerCase() === t) return L.folderName;
|
|
544
|
+
if (`cloud-ide-${t}` === L.folderName.toLowerCase()) return L.folderName;
|
|
545
|
+
if (L.packageName && L.packageName.toLowerCase() === t) return L.folderName;
|
|
546
|
+
const scoped = `@cloudidesys/cloud-ide-${t}`;
|
|
547
|
+
if (L.packageName && L.packageName.toLowerCase() === scoped) return L.folderName;
|
|
548
|
+
}
|
|
549
|
+
return token;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function printStartupScan(ws) {
|
|
553
|
+
const cliVersion = require('./package.json').version;
|
|
554
|
+
console.log('');
|
|
555
|
+
console.log('============================================================');
|
|
556
|
+
console.log(` CloudIDE Workspace Shell (v${cliVersion})`);
|
|
557
|
+
console.log(' Git · build · publish · server · npm — type "help" for commands');
|
|
558
|
+
console.log('============================================================');
|
|
559
|
+
if (!ws.angularRoot) {
|
|
560
|
+
console.log(
|
|
561
|
+
'\n (No Angular workspace found. cd into the folder that has angular cide.json, or run from a monorepo parent that contains it.)\n'
|
|
562
|
+
);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
console.log(`\n Angular root: ${ws.angularRoot}`);
|
|
566
|
+
console.log(` Monorepo: ${ws.monorepoRoot}`);
|
|
567
|
+
console.log(` Angular libs: ${ws.angularLibs.length} (auto-discovered via projects/*/ng-package.json)`);
|
|
568
|
+
if (ws.nodeBackend) console.log(` Node API: ${ws.nodeBackend.name}`);
|
|
569
|
+
console.log('\n Repos:');
|
|
570
|
+
for (const r of ws.repos) {
|
|
571
|
+
const { hint } = gitRepoStatusSummary(r.absPath);
|
|
572
|
+
console.log(` * ${r.name.padEnd(22)} ${hint}`);
|
|
573
|
+
}
|
|
574
|
+
console.log('');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function runGitAll(ws, args, inherit) {
|
|
578
|
+
for (const r of ws.repos) {
|
|
579
|
+
console.log(`\n--- ${r.name} ---`);
|
|
580
|
+
try {
|
|
581
|
+
execSync(`git ${args}`, { cwd: r.absPath, stdio: inherit ? 'inherit' : 'pipe', shell: true });
|
|
582
|
+
} catch (e) {
|
|
583
|
+
console.error(` (git failed in ${r.name})`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function runGitOne(ws, repoName, args, inherit) {
|
|
589
|
+
const r = findRepo(ws, repoName);
|
|
590
|
+
if (!r) {
|
|
591
|
+
console.error(`Unknown repo: ${repoName}. Use: ${ws.repos.map((x) => x.name).join(', ')}`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
execSync(`git ${args}`, { cwd: r.absPath, stdio: inherit ? 'inherit' : 'pipe', shell: true });
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function handleStatus(ws) {
|
|
598
|
+
for (const r of ws.repos) {
|
|
599
|
+
const st = gitShortStatus(r.absPath);
|
|
600
|
+
const line = st && st.length ? st.split('\n').slice(0, 5).join('; ') : '(clean)';
|
|
601
|
+
const more = st && st.split('\n').length > 5 ? '...' : '';
|
|
602
|
+
console.log(` ${r.name.padEnd(22)} ${line}${more}`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function handleCommit(ws, message) {
|
|
607
|
+
if (!message || !String(message).trim()) {
|
|
608
|
+
console.error('Usage: commit "your message"');
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const msg = String(message).trim();
|
|
612
|
+
for (const r of ws.repos) {
|
|
613
|
+
const st = gitShortStatus(r.absPath);
|
|
614
|
+
if (!st || !st.length) {
|
|
615
|
+
console.log(` ${r.name}: (no changes, skip)`);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
console.log(` ${r.name}: committing...`);
|
|
619
|
+
try {
|
|
620
|
+
execSync('git add -A', { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
621
|
+
execSync(`git commit -m ${JSON.stringify(msg)}`, { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
622
|
+
console.log(` done`);
|
|
623
|
+
} catch {
|
|
624
|
+
console.log(` (commit skipped or failed — maybe nothing staged)`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function handlePush(ws, repoName) {
|
|
630
|
+
if (repoName) {
|
|
631
|
+
runGitOne(ws, repoName, 'push', true);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
for (const r of ws.repos) {
|
|
635
|
+
const ahead = gitAheadCount(r.absPath);
|
|
636
|
+
if (ahead <= 0) {
|
|
637
|
+
console.log(` ${r.name}: up to date`);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
console.log(` ${r.name}: ${ahead} ahead — pushing...`);
|
|
641
|
+
try {
|
|
642
|
+
execSync('git push', { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
643
|
+
console.log(` done`);
|
|
644
|
+
} catch {
|
|
645
|
+
console.error(` failed`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function handlePull(ws, repoName) {
|
|
651
|
+
if (repoName) {
|
|
652
|
+
runGitOne(ws, repoName, 'pull', true);
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
runGitAll(ws, 'pull', true);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function handleDiff(ws, repoName) {
|
|
659
|
+
if (repoName) runGitOne(ws, repoName, 'diff', true);
|
|
660
|
+
else runGitAll(ws, 'diff', true);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function handleLog(ws, repoName) {
|
|
664
|
+
const args = 'log --oneline -5';
|
|
665
|
+
if (repoName) runGitOne(ws, repoName, args, true);
|
|
666
|
+
else runGitAll(ws, args, true);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function handleStash(ws, repoName) {
|
|
670
|
+
if (repoName) runGitOne(ws, repoName, 'stash', true);
|
|
671
|
+
else runGitAll(ws, 'stash', true);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function handleBranch(ws, repoName) {
|
|
675
|
+
if (repoName) runGitOne(ws, repoName, 'branch -vv', true);
|
|
676
|
+
else runGitAll(ws, 'branch -vv', true);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function killBgServer() {
|
|
680
|
+
if (!bgServerPid) {
|
|
681
|
+
console.log('No background server PID tracked (start with: server dev)');
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
if (process.platform === 'win32') {
|
|
686
|
+
execSync(`taskkill /PID ${bgServerPid} /T /F`, { stdio: 'inherit', shell: true });
|
|
687
|
+
} else {
|
|
688
|
+
process.kill(-bgServerPid, 'SIGTERM');
|
|
689
|
+
}
|
|
690
|
+
console.log(`Stopped process tree ${bgServerPid}`);
|
|
691
|
+
} catch (e) {
|
|
692
|
+
console.error('Stop failed:', e.message);
|
|
693
|
+
}
|
|
694
|
+
bgServerPid = null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function spawnBg(cwd, command, args) {
|
|
698
|
+
const child = spawn(command, args, {
|
|
699
|
+
cwd,
|
|
700
|
+
shell: true,
|
|
701
|
+
detached: true,
|
|
702
|
+
stdio: 'ignore',
|
|
703
|
+
});
|
|
704
|
+
child.unref();
|
|
705
|
+
bgServerPid = child.pid;
|
|
706
|
+
console.log(`Started background PID ${child.pid} (${command} ${args.join(' ')})`);
|
|
707
|
+
console.log('(Output not shown here; use server stop to end, or run npm in another terminal for logs.)');
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function handleServer(ws, sub, rest, rl) {
|
|
711
|
+
// init & listener don't need a node backend — they manage the .cide/ upload server
|
|
712
|
+
if (sub === 'init') {
|
|
713
|
+
const type = rest[0] || '';
|
|
714
|
+
const targetPath = rest[1] || '';
|
|
715
|
+
await serverInit({ type, path: targetPath, readlineInterface: rl });
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (sub === 'listener') {
|
|
719
|
+
const action = (rest[0] || '').toLowerCase();
|
|
720
|
+
const { findCideDir, startNodeListener, stopNodeListener, listenerStatus } = serverInit;
|
|
721
|
+
const cideDir = findCideDir();
|
|
722
|
+
if (!cideDir) {
|
|
723
|
+
console.error(' No .cide/ folder found. Run: server init first.');
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (action === 'start') startNodeListener(cideDir);
|
|
727
|
+
else if (action === 'stop') stopNodeListener(cideDir);
|
|
728
|
+
else if (action === 'restart') { stopNodeListener(cideDir); startNodeListener(cideDir); }
|
|
729
|
+
else if (action === 'status') listenerStatus(cideDir);
|
|
730
|
+
else console.error(' Usage: server listener start | stop | restart | status');
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// build/start/dev/stop need the node backend project
|
|
735
|
+
const nb = ws.nodeBackend;
|
|
736
|
+
if (!nb) {
|
|
737
|
+
console.error('No Node backend found (cide.json with templete: node).');
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const cwd = nb.absPath;
|
|
741
|
+
if (sub === 'build') {
|
|
742
|
+
execSync('npm run build', { cwd, stdio: 'inherit', shell: true });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (sub === 'start') {
|
|
746
|
+
spawnBg(cwd, 'npm', ['run', 'start']);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (sub === 'dev') {
|
|
750
|
+
spawnBg(cwd, 'npm', ['run', 'dev']);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (sub === 'stop') {
|
|
754
|
+
killBgServer();
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
console.error('Usage: server build | start | dev | stop | init | listener start|stop|status|restart');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function handleSeed(ws, name) {
|
|
761
|
+
const nb = ws.nodeBackend;
|
|
762
|
+
if (!nb) {
|
|
763
|
+
console.error('No Node backend found.');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (!name) {
|
|
767
|
+
console.log('Seed scripts:');
|
|
768
|
+
for (const s of ws.seedScripts) console.log(` ${s.name}`);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const hit = ws.seedScripts.find(
|
|
772
|
+
(s) =>
|
|
773
|
+
s.name === name ||
|
|
774
|
+
s.name === `seed-${name}` ||
|
|
775
|
+
s.file === `${name}.ts` ||
|
|
776
|
+
s.file === `seed-${name}.ts`
|
|
777
|
+
);
|
|
778
|
+
if (!hit) {
|
|
779
|
+
console.error(`Unknown seed: ${name}`);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const rel = `src/scripts/${hit.file}`;
|
|
783
|
+
console.log(`Running: npx ts-node ${rel}`);
|
|
784
|
+
execSync(`npx ts-node ${rel}`, { cwd: nb.absPath, stdio: 'inherit', shell: true });
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function readAngularWorkspaceScripts(ws) {
|
|
788
|
+
const pj = path.join(ws.angularRoot, 'package.json');
|
|
789
|
+
const pkg = safeReadJson(pj);
|
|
790
|
+
return (pkg && pkg.scripts) || {};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function handleServe(ws) {
|
|
794
|
+
const scripts = readAngularWorkspaceScripts(ws);
|
|
795
|
+
if (!scripts.start) {
|
|
796
|
+
console.error(`No "start" script in ${path.basename(ws.angularRoot)}/package.json`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
spawnBg(ws.angularRoot, 'npm', ['run', 'start']);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function handleTailwind(ws) {
|
|
803
|
+
const scripts = readAngularWorkspaceScripts(ws);
|
|
804
|
+
if (!scripts.tailwindcss) {
|
|
805
|
+
console.error(`No "tailwindcss" script in ${path.basename(ws.angularRoot)}/package.json`);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
spawnBg(ws.angularRoot, 'npm', ['run', 'tailwindcss']);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function handleLint(ws) {
|
|
812
|
+
execSync('npm run lint', { cwd: ws.angularRoot, stdio: 'inherit', shell: true });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function handleTest(ws) {
|
|
816
|
+
execSync('npm run test', { cwd: ws.angularRoot, stdio: 'inherit', shell: true });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function handleAnalyze(ws) {
|
|
820
|
+
const scripts = readAngularWorkspaceScripts(ws);
|
|
821
|
+
const key = scripts['build:analyze'] ? 'build:analyze' : null;
|
|
822
|
+
if (!key) {
|
|
823
|
+
console.error('No build:analyze script.');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
execSync(`npm run ${key}`, { cwd: ws.angularRoot, stdio: 'inherit', shell: true });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function handleNpm(ws, tokens) {
|
|
830
|
+
// npm install <repo> | npm install all | npm update <repo> | npm outdated <repo> | npm list <repo>
|
|
831
|
+
// Optional --force anywhere after "npm" for install | update
|
|
832
|
+
const force = tokens.some((t) => t === '--force');
|
|
833
|
+
const filtered = tokens.filter((t) => t !== '--force');
|
|
834
|
+
const sub = filtered[1]?.toLowerCase();
|
|
835
|
+
const target = filtered[2];
|
|
836
|
+
const forceSuffix = force ? ' --force' : '';
|
|
837
|
+
if (sub === 'install' && target && target.toLowerCase() === 'all') {
|
|
838
|
+
for (const r of ws.repos) {
|
|
839
|
+
console.log(`\nnpm install in ${r.name}...${force ? ' (--force)' : ''}`);
|
|
840
|
+
execSync(`npm install${forceSuffix}`, { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
841
|
+
}
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (!target) {
|
|
845
|
+
console.error('Usage: npm install <repoName|all> [--force] | npm update <repo> [--force] | npm outdated <repo> | npm list <repo>');
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const r = findRepo(ws, target);
|
|
849
|
+
if (!r) {
|
|
850
|
+
console.error(`Unknown repo: ${target}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (sub === 'install') execSync(`npm install${forceSuffix}`, { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
854
|
+
else if (sub === 'update') execSync(`npm update${forceSuffix}`, { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
855
|
+
else if (sub === 'outdated') execSync('npm outdated', { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
856
|
+
else if (sub === 'list') execSync('npm list --depth=0', { cwd: r.absPath, stdio: 'inherit', shell: true });
|
|
857
|
+
else console.error('Unknown npm subcommand');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function handleEnv(ws, sub) {
|
|
861
|
+
const nb = ws.nodeBackend;
|
|
862
|
+
if (!nb) {
|
|
863
|
+
console.error('No Node backend.');
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const envPath = path.join(nb.absPath, '.env');
|
|
867
|
+
if (sub === 'show') {
|
|
868
|
+
if (!existsSync(envPath)) {
|
|
869
|
+
console.log('No .env file.');
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
const raw = fs.readFileSync(envPath, 'utf8');
|
|
873
|
+
const keys = [];
|
|
874
|
+
for (const line of raw.split('\n')) {
|
|
875
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
876
|
+
if (m) keys.push(m[1]);
|
|
877
|
+
}
|
|
878
|
+
console.log('Keys in .env:', keys.join(', ') || '(none)');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (sub === 'check') {
|
|
882
|
+
if (!existsSync(envPath)) {
|
|
883
|
+
console.log('MISSING .env file at', envPath);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const raw = fs.readFileSync(envPath, 'utf8');
|
|
887
|
+
const map = {};
|
|
888
|
+
for (const line of raw.split('\n')) {
|
|
889
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
890
|
+
if (m) map[m[1]] = m[2].trim();
|
|
891
|
+
}
|
|
892
|
+
for (const k of ENV_KEYS_TO_CHECK) {
|
|
893
|
+
const v = map[k];
|
|
894
|
+
const ok = v && v.length && v !== '""' && v !== "''";
|
|
895
|
+
console.log(` ${k.padEnd(22)} ${ok ? 'OK' : 'MISSING or empty'}`);
|
|
896
|
+
}
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
console.error('Usage: env check | env show');
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function handleLibs(ws) {
|
|
903
|
+
for (const L of ws.angularLibs) {
|
|
904
|
+
console.log(` ${L.folderName.padEnd(28)} ${String(L.packageName).padEnd(44)} v${L.version}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function handlePackages(ws) {
|
|
909
|
+
if (!ws.angularRoot) return;
|
|
910
|
+
const list = ws.publishList;
|
|
911
|
+
list.forEach((p, i) => {
|
|
912
|
+
const rel = p.labelRel || p.folderName;
|
|
913
|
+
console.log(` [${i + 1}] ${p.name.padEnd(42)} v${p.version} (${rel})`);
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function handleRepos(ws) {
|
|
918
|
+
for (const r of ws.repos) {
|
|
919
|
+
let remote = '';
|
|
920
|
+
try {
|
|
921
|
+
remote = execSync('git remote get-url origin', { cwd: r.absPath, encoding: 'utf8', shell: true }).trim();
|
|
922
|
+
} catch {
|
|
923
|
+
remote = '(no origin)';
|
|
924
|
+
}
|
|
925
|
+
console.log(` ${r.name.padEnd(22)} ${remote}`);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function handleVersions(ws) {
|
|
930
|
+
handlePackages(ws);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function handleDeps(ws, pkgQuery) {
|
|
934
|
+
if (!pkgQuery) {
|
|
935
|
+
console.error('Usage: deps <package-name>');
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const needle = pkgQuery.trim();
|
|
939
|
+
const roots = [ws.monorepoRoot, ws.angularRoot].filter(Boolean);
|
|
940
|
+
const seen = new Set();
|
|
941
|
+
function scanFile(pjPath) {
|
|
942
|
+
const pkg = safeReadJson(pjPath);
|
|
943
|
+
if (!pkg) return;
|
|
944
|
+
const sections = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
945
|
+
for (const sec of sections) {
|
|
946
|
+
const o = pkg[sec];
|
|
947
|
+
if (!o || typeof o !== 'object') continue;
|
|
948
|
+
if (Object.prototype.hasOwnProperty.call(o, needle)) {
|
|
949
|
+
console.log(` ${path.relative(ws.monorepoRoot, pjPath) || pjPath} [${sec}]`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function walk(dir, depth) {
|
|
954
|
+
if (depth > 6) return;
|
|
955
|
+
let entries;
|
|
956
|
+
try {
|
|
957
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
958
|
+
} catch {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
for (const e of entries) {
|
|
962
|
+
if (e.name === 'node_modules' || e.name === '.git' || e.name === 'dist' || e.name === 'A-RND') continue;
|
|
963
|
+
const full = path.join(dir, e.name);
|
|
964
|
+
if (e.isDirectory()) walk(full, depth + 1);
|
|
965
|
+
else if (e.name === 'package.json') {
|
|
966
|
+
const rp = path.resolve(full);
|
|
967
|
+
if (seen.has(rp)) continue;
|
|
968
|
+
seen.add(rp);
|
|
969
|
+
scanFile(rp);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
for (const root of roots) {
|
|
974
|
+
if (existsSync(path.join(root, 'package.json'))) scanFile(path.join(root, 'package.json'));
|
|
975
|
+
walk(root, 0);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
async function runBuild(ws, name) {
|
|
980
|
+
if (!name) {
|
|
981
|
+
console.error('Usage: build <short-name|folder|scoped-package> e.g. build fees');
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const folder = resolveBuildToken(name, ws);
|
|
985
|
+
const prev = process.cwd();
|
|
986
|
+
try {
|
|
987
|
+
process.chdir(ws.angularRoot);
|
|
988
|
+
await buildProject(folder, false);
|
|
989
|
+
} finally {
|
|
990
|
+
process.chdir(prev);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async function runBuildAll(ws) {
|
|
995
|
+
const prev = process.cwd();
|
|
996
|
+
try {
|
|
997
|
+
process.chdir(ws.angularRoot);
|
|
998
|
+
await buildAllProjects(false);
|
|
999
|
+
} finally {
|
|
1000
|
+
process.chdir(prev);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/** Old cide start behavior (ts-node-dev from cide.json) — use: start legacy */
|
|
1005
|
+
function handleStartLegacyProject(ws) {
|
|
1006
|
+
const prev = process.cwd();
|
|
1007
|
+
try {
|
|
1008
|
+
if (ws.nodeBackend) process.chdir(ws.nodeBackend.absPath);
|
|
1009
|
+
startProject();
|
|
1010
|
+
} finally {
|
|
1011
|
+
process.chdir(prev);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function handleWatch(ws, args) {
|
|
1016
|
+
const prev = process.cwd();
|
|
1017
|
+
try {
|
|
1018
|
+
process.chdir(ws.angularRoot);
|
|
1019
|
+
process.argv = ['node', 'cide', 'watch', ...(args || [])];
|
|
1020
|
+
watchLinkProject();
|
|
1021
|
+
} finally {
|
|
1022
|
+
process.chdir(prev);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function tokenize(line) {
|
|
1027
|
+
const t = line.trim();
|
|
1028
|
+
if (!t) return [];
|
|
1029
|
+
const parts = [];
|
|
1030
|
+
let i = 0;
|
|
1031
|
+
while (i < t.length) {
|
|
1032
|
+
while (i < t.length && /\s/.test(t[i])) i++;
|
|
1033
|
+
if (i >= t.length) break;
|
|
1034
|
+
if (t[i] === '"') {
|
|
1035
|
+
i++;
|
|
1036
|
+
let s = '';
|
|
1037
|
+
while (i < t.length && t[i] !== '"') s += t[i++];
|
|
1038
|
+
if (t[i] === '"') i++;
|
|
1039
|
+
parts.push(s);
|
|
1040
|
+
} else {
|
|
1041
|
+
let s = '';
|
|
1042
|
+
while (i < t.length && !/\s/.test(t[i])) s += t[i++];
|
|
1043
|
+
parts.push(s);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return parts;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function printHelp(group) {
|
|
1050
|
+
const g = (group || '').toLowerCase();
|
|
1051
|
+
const all = !g || g === 'all';
|
|
1052
|
+
if (all || g === 'git') {
|
|
1053
|
+
console.log(`
|
|
1054
|
+
Git: status | commit "msg" | push [repo] | pull [repo] | diff [repo] | log [repo] | stash [repo] | branch [repo]`);
|
|
1055
|
+
}
|
|
1056
|
+
if (all || g === 'build') {
|
|
1057
|
+
console.log(`
|
|
1058
|
+
Build: build [-p 1|all|…] [--resume-from N] | build <name> | build-all | libs`);
|
|
1059
|
+
}
|
|
1060
|
+
if (all || g === 'publish') {
|
|
1061
|
+
console.log(`
|
|
1062
|
+
Publish: publish [--force] | install-global [--force]
|
|
1063
|
+
On publish failure: use --retry-from N (same step, no double bump) or --resume-from N (next step)`);
|
|
1064
|
+
}
|
|
1065
|
+
if (all || g === 'server') {
|
|
1066
|
+
console.log(`
|
|
1067
|
+
Server: server build | start | dev | stop | init | listener start|stop|status|restart | seed [name]`);
|
|
1068
|
+
}
|
|
1069
|
+
if (all || g === 'upload') {
|
|
1070
|
+
console.log(`
|
|
1071
|
+
Upload: upload [N] | upload --list | upload --history | upload --rollback`);
|
|
1072
|
+
}
|
|
1073
|
+
if (all || g === 'angular' || g === 'ui') {
|
|
1074
|
+
console.log(`
|
|
1075
|
+
Angular workspace: serve | tailwind | lint | test | analyze`);
|
|
1076
|
+
}
|
|
1077
|
+
if (all || g === 'npm') {
|
|
1078
|
+
console.log(`
|
|
1079
|
+
npm: npm install <repo>|all [--force] | npm update <repo> [--force] | npm outdated <repo> | npm list <repo>`);
|
|
1080
|
+
}
|
|
1081
|
+
if (all || g === 'env') {
|
|
1082
|
+
console.log(`
|
|
1083
|
+
Env: env check | env show`);
|
|
1084
|
+
}
|
|
1085
|
+
if (all || g === 'info') {
|
|
1086
|
+
console.log(`
|
|
1087
|
+
Info: packages | repos | versions | deps <pkg>`);
|
|
1088
|
+
}
|
|
1089
|
+
if (all || g === 'start') {
|
|
1090
|
+
console.log(`
|
|
1091
|
+
Start: start — list Angular / Node / Tailwind; in IDE: paste commands in new terminal; else: new OS window (Win)
|
|
1092
|
+
start 1 | start 1,3 | start 1-3 | start all — skip the menu
|
|
1093
|
+
start legacy — old startProject() from cide.json`);
|
|
1094
|
+
}
|
|
1095
|
+
if (all) {
|
|
1096
|
+
console.log(`
|
|
1097
|
+
Other: help [group] | clear | exit`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* @returns {Promise<{ skipPrompt?: boolean }|void>}
|
|
1103
|
+
*/
|
|
1104
|
+
async function dispatchLine(ws, line, rl) {
|
|
1105
|
+
const tokens = tokenize(line);
|
|
1106
|
+
if (!tokens.length) return;
|
|
1107
|
+
const cmd = tokens[0].toLowerCase();
|
|
1108
|
+
const rest = tokens.slice(1);
|
|
1109
|
+
|
|
1110
|
+
if (cmd === 'exit' || cmd === 'quit') {
|
|
1111
|
+
// Must set before rl.close(): 'close' can fire synchronously; if _shellExiting is still false,
|
|
1112
|
+
// the handler would reopen readline and "exit" would appear to do nothing (quit looked flaky too).
|
|
1113
|
+
_shellExiting = true;
|
|
1114
|
+
rl.close();
|
|
1115
|
+
return { skipPrompt: true };
|
|
1116
|
+
}
|
|
1117
|
+
if (cmd === 'clear') {
|
|
1118
|
+
console.clear();
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (cmd === 'help') {
|
|
1122
|
+
printHelp(rest[0]);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (!ws.angularRoot && !['help', 'exit', 'quit', 'clear', 'server', 'upload'].includes(cmd)) {
|
|
1127
|
+
console.error(
|
|
1128
|
+
'No Angular workspace found. cd into a folder with angular cide.json (or a monorepo parent that contains it), then run cide shell again.'
|
|
1129
|
+
);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
try {
|
|
1134
|
+
if (cmd === 'status') handleStatus(ws);
|
|
1135
|
+
else if (cmd === 'commit') handleCommit(ws, rest.join(' '));
|
|
1136
|
+
else if (cmd === 'push') handlePush(ws, rest[0]);
|
|
1137
|
+
else if (cmd === 'pull') handlePull(ws, rest[0]);
|
|
1138
|
+
else if (cmd === 'diff') handleDiff(ws, rest[0]);
|
|
1139
|
+
else if (cmd === 'log') handleLog(ws, rest[0]);
|
|
1140
|
+
else if (cmd === 'stash') handleStash(ws, rest[0]);
|
|
1141
|
+
else if (cmd === 'branch') handleBranch(ws, rest[0]);
|
|
1142
|
+
else if (cmd === 'build-all' || cmd === 'buildall') await runBuildAll(ws);
|
|
1143
|
+
else if (cmd === 'build') {
|
|
1144
|
+
const prev = process.cwd();
|
|
1145
|
+
try {
|
|
1146
|
+
process.chdir(ws.angularRoot);
|
|
1147
|
+
const hasFlag = rest.some((r) => String(r).startsWith('-'));
|
|
1148
|
+
if (rest.length === 0 || hasFlag) {
|
|
1149
|
+
const opts = parseBuildShellArgs(rest);
|
|
1150
|
+
await buildWorkspace({ readlineInterface: rl, ...opts });
|
|
1151
|
+
} else {
|
|
1152
|
+
await runBuild(ws, rest[0]);
|
|
1153
|
+
}
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
console.error(e.message || e);
|
|
1156
|
+
} finally {
|
|
1157
|
+
process.chdir(prev);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
else if (cmd === 'libs') handleLibs(ws);
|
|
1161
|
+
else if (cmd === 'publish') {
|
|
1162
|
+
const prev = process.cwd();
|
|
1163
|
+
try {
|
|
1164
|
+
process.chdir(ws.angularRoot);
|
|
1165
|
+
const force = rest.includes('--force');
|
|
1166
|
+
await publishPackage({ readlineInterface: rl, force });
|
|
1167
|
+
} catch (e) {
|
|
1168
|
+
console.error(e);
|
|
1169
|
+
} finally {
|
|
1170
|
+
process.chdir(prev);
|
|
1171
|
+
}
|
|
1172
|
+
return { skipPrompt: false };
|
|
1173
|
+
} else if (cmd === 'install-global') {
|
|
1174
|
+
const prev = process.cwd();
|
|
1175
|
+
try {
|
|
1176
|
+
process.chdir(ws.angularRoot);
|
|
1177
|
+
const force = rest.includes('--force');
|
|
1178
|
+
await installGlobalPackage({ readlineInterface: rl, force });
|
|
1179
|
+
} catch (e) {
|
|
1180
|
+
console.error(e);
|
|
1181
|
+
} finally {
|
|
1182
|
+
process.chdir(prev);
|
|
1183
|
+
}
|
|
1184
|
+
return { skipPrompt: false };
|
|
1185
|
+
} else if (cmd === 'server') await handleServer(ws, rest[0]?.toLowerCase(), rest.slice(1), rl);
|
|
1186
|
+
else if (cmd === 'seed') handleSeed(ws, rest[0]);
|
|
1187
|
+
else if (cmd === 'serve') handleServe(ws);
|
|
1188
|
+
else if (cmd === 'tailwind') handleTailwind(ws);
|
|
1189
|
+
else if (cmd === 'lint') handleLint(ws);
|
|
1190
|
+
else if (cmd === 'test') handleTest(ws);
|
|
1191
|
+
else if (cmd === 'analyze') handleAnalyze(ws);
|
|
1192
|
+
else if (cmd === 'npm') handleNpm(ws, tokens);
|
|
1193
|
+
else if (cmd === 'env') handleEnv(ws, rest[0]?.toLowerCase());
|
|
1194
|
+
else if (cmd === 'packages') handlePackages(ws);
|
|
1195
|
+
else if (cmd === 'repos') handleRepos(ws);
|
|
1196
|
+
else if (cmd === 'versions') handleVersions(ws);
|
|
1197
|
+
else if (cmd === 'deps') handleDeps(ws, rest[0]);
|
|
1198
|
+
else if (cmd === 'start') await handleStartCommand(ws, rl, rest);
|
|
1199
|
+
else if (cmd === 'watch') handleWatch(ws, rest);
|
|
1200
|
+
else if (cmd === 'upload') {
|
|
1201
|
+
const uploadOpts = { readlineInterface: rl };
|
|
1202
|
+
if (rest.includes('--list')) uploadOpts.list = true;
|
|
1203
|
+
else if (rest.includes('--history')) uploadOpts.history = true;
|
|
1204
|
+
else if (rest.includes('--rollback')) uploadOpts.rollback = true;
|
|
1205
|
+
// Parse --server flag value
|
|
1206
|
+
const serverIdx = rest.indexOf('--server');
|
|
1207
|
+
if (serverIdx !== -1 && rest[serverIdx + 1]) uploadOpts.server = rest[serverIdx + 1];
|
|
1208
|
+
// Parse -m / --message flag value
|
|
1209
|
+
let msgIdx = rest.indexOf('-m');
|
|
1210
|
+
if (msgIdx === -1) msgIdx = rest.indexOf('--message');
|
|
1211
|
+
if (msgIdx !== -1 && rest[msgIdx + 1]) uploadOpts.message = rest[msgIdx + 1];
|
|
1212
|
+
// First non-flag token (that isn't the --server value) is the project number
|
|
1213
|
+
const num = rest.find((r, i) => !String(r).startsWith('-') && i !== serverIdx + 1);
|
|
1214
|
+
if (num) uploadOpts.packages = num;
|
|
1215
|
+
await uploadProject(uploadOpts);
|
|
1216
|
+
}
|
|
1217
|
+
else console.error(`Unknown command: "${line.trim()}". Type "help".`);
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
console.error(e.message || e);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
let _shellExiting = false;
|
|
1224
|
+
|
|
1225
|
+
function openReadline(ws) {
|
|
1226
|
+
// Keep stdin alive so the process never ends unless the user types exit/quit
|
|
1227
|
+
process.stdin.resume();
|
|
1228
|
+
|
|
1229
|
+
const rl = readline.createInterface({
|
|
1230
|
+
input: process.stdin,
|
|
1231
|
+
output: process.stdout,
|
|
1232
|
+
prompt: PROMPT,
|
|
1233
|
+
historySize: 100,
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
let busy = false;
|
|
1237
|
+
|
|
1238
|
+
function safePrompt() {
|
|
1239
|
+
if (!busy && !_shellExiting) rl.prompt();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
rl.prompt();
|
|
1243
|
+
|
|
1244
|
+
rl.on('line', async (raw) => {
|
|
1245
|
+
const line = raw.trim();
|
|
1246
|
+
if (!line) {
|
|
1247
|
+
safePrompt();
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
// Queue the command; if busy just wait (sequentially)
|
|
1251
|
+
busy = true;
|
|
1252
|
+
try {
|
|
1253
|
+
const r = await dispatchLine(ws, line, rl);
|
|
1254
|
+
if (r && r.skipPrompt) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
} catch (e) {
|
|
1258
|
+
console.error(e.message || e);
|
|
1259
|
+
} finally {
|
|
1260
|
+
busy = false;
|
|
1261
|
+
if (!_shellExiting) safePrompt();
|
|
1262
|
+
}
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
rl.on('close', () => {
|
|
1266
|
+
if (_shellExiting) {
|
|
1267
|
+
console.log('Goodbye.');
|
|
1268
|
+
process.exit(0);
|
|
1269
|
+
}
|
|
1270
|
+
// stdin was closed by the terminal emulator but user did NOT type exit.
|
|
1271
|
+
// Re-open readline so the session persists.
|
|
1272
|
+
process.stdout.write('\n');
|
|
1273
|
+
setImmediate(() => openReadline(ws));
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
// Ctrl+C — show prompt again, do NOT exit
|
|
1277
|
+
process.removeAllListeners('SIGINT');
|
|
1278
|
+
process.on('SIGINT', () => {
|
|
1279
|
+
process.stdout.write('\n(Ctrl+C caught — type "exit" or "quit" to leave)\n');
|
|
1280
|
+
busy = false;
|
|
1281
|
+
safePrompt();
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function startShell() {
|
|
1286
|
+
_shellExiting = false;
|
|
1287
|
+
const ws = loadWorkspace();
|
|
1288
|
+
printStartupScan(ws);
|
|
1289
|
+
openReadline(ws);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
module.exports = { startShell, loadWorkspace, tokenize, discoverRunnableProjects };
|