proteum 2.1.8 → 2.1.9-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/paths.ts CHANGED
@@ -35,6 +35,29 @@ export type TPathInfos = {
35
35
  isIndex: boolean;
36
36
  };
37
37
 
38
+ export type TResolvedPackageBinary = {
39
+ packageName: string;
40
+ packageRoot: string;
41
+ binPath: string;
42
+ command: string;
43
+ args: string[];
44
+ };
45
+
46
+ export type TFrameworkInstallGraph = {
47
+ activeRoot: string;
48
+ installedRoot?: string;
49
+ appNodeModulesRoot: string;
50
+ frameworkNodeModulesRoot: string;
51
+ };
52
+
53
+ export type TFrameworkInstallMode = 'npm' | 'npm-link' | 'path' | 'workspace' | 'global' | 'checkout';
54
+
55
+ export type TFrameworkInstallInfo = {
56
+ mode: TFrameworkInstallMode;
57
+ summary: string;
58
+ dependencySpec?: string;
59
+ };
60
+
38
61
  /*----------------------------------
39
62
  - CONFIG
40
63
  ----------------------------------*/
@@ -51,7 +74,88 @@ const safeRealpath = (filepath: string) => {
51
74
  }
52
75
  };
53
76
 
54
- const resolveCoreRoot = (appRoot: string) => {
77
+ const readPackageJson = (filepath: string) => {
78
+ try {
79
+ return JSON.parse(fs.readFileSync(filepath, 'utf8')) as Record<string, unknown>;
80
+ } catch {
81
+ return undefined;
82
+ }
83
+ };
84
+
85
+ const readPackageDependencySpec = (appRoot: string, packageName: string) => {
86
+ const packageJson = readPackageJson(path.join(appRoot, 'package.json'));
87
+ if (!packageJson) return undefined;
88
+
89
+ const dependencySections = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'] as const;
90
+
91
+ for (const section of dependencySections) {
92
+ const dependencies = packageJson[section];
93
+ if (!dependencies || typeof dependencies !== 'object' || Array.isArray(dependencies)) continue;
94
+
95
+ const dependencySpec = (dependencies as Record<string, unknown>)[packageName];
96
+ if (typeof dependencySpec !== 'string' || dependencySpec.trim() === '') continue;
97
+
98
+ return dependencySpec.trim();
99
+ }
100
+
101
+ return undefined;
102
+ };
103
+
104
+ const findVisibleNodeModulesRoot = (startPath: string): string | undefined => {
105
+ let currentPath = path.resolve(startPath);
106
+
107
+ while (true) {
108
+ const candidate = path.join(currentPath, 'node_modules');
109
+ if (fs.existsSync(candidate)) return candidate;
110
+
111
+ const parentPath = path.dirname(currentPath);
112
+ if (parentPath === currentPath) return undefined;
113
+ currentPath = parentPath;
114
+ }
115
+ };
116
+
117
+ const findVisibleNodeModulesRoots = (startPath: string): string[] => {
118
+ const roots: string[] = [];
119
+ let currentPath = path.resolve(startPath);
120
+
121
+ while (true) {
122
+ const candidate = path.join(currentPath, 'node_modules');
123
+ if (fs.existsSync(candidate) && !roots.includes(candidate)) roots.push(candidate);
124
+
125
+ const parentPath = path.dirname(currentPath);
126
+ if (parentPath === currentPath) return roots;
127
+ currentPath = parentPath;
128
+ }
129
+ };
130
+
131
+ const findVisiblePackageInstall = (startPath: string, packageName: string): string | undefined => {
132
+ let currentPath = path.resolve(startPath);
133
+
134
+ while (true) {
135
+ const candidate = path.join(currentPath, 'node_modules', packageName);
136
+ if (fs.existsSync(candidate)) return candidate;
137
+
138
+ const parentPath = path.dirname(currentPath);
139
+ if (parentPath === currentPath) return undefined;
140
+ currentPath = parentPath;
141
+ }
142
+ };
143
+
144
+ const findVisiblePackageInstalls = (startPath: string, packageName: string): string[] => {
145
+ const installs: string[] = [];
146
+ let currentPath = path.resolve(startPath);
147
+
148
+ while (true) {
149
+ const candidate = path.join(currentPath, 'node_modules', packageName);
150
+ if (fs.existsSync(candidate) && !installs.includes(candidate)) installs.push(candidate);
151
+
152
+ const parentPath = path.dirname(currentPath);
153
+ if (parentPath === currentPath) return installs;
154
+ currentPath = parentPath;
155
+ }
156
+ };
157
+
158
+ const resolveCoreRoot = (appRoot: string): string => {
55
159
  const currentPackageRoot = path.resolve(__dirname, '..');
56
160
  const currentBin = path.join(currentPackageRoot, 'cli', 'bin.js');
57
161
  const invokedScript = process.argv[1] ? safeRealpath(process.argv[1]) : '';
@@ -59,8 +163,8 @@ const resolveCoreRoot = (appRoot: string) => {
59
163
 
60
164
  if (invokedCurrentPackage) return currentPackageRoot;
61
165
 
62
- const localInstall = path.join(appRoot, 'node_modules', 'proteum');
63
- if (fs.existsSync(localInstall)) return localInstall;
166
+ const installedFrameworkRoot = findVisiblePackageInstall(appRoot, 'proteum');
167
+ if (installedFrameworkRoot) return installedFrameworkRoot;
64
168
 
65
169
  // When running `npx`/global installs, there may be no local `node_modules/proteum` yet.
66
170
  // Fall back to the installed package root (this file lives in `<root>/cli`).
@@ -69,9 +173,101 @@ const resolveCoreRoot = (appRoot: string) => {
69
173
 
70
174
  const normalizeImportPath = (value: string) => value.replace(/\\/g, '/');
71
175
 
176
+ const resolveAppNodeModulesRoot = (appRoot: string): string => {
177
+ const installedRoot = findVisiblePackageInstall(appRoot, 'proteum');
178
+
179
+ return (
180
+ (installedRoot ? path.dirname(installedRoot) : undefined) ||
181
+ findVisibleNodeModulesRoot(appRoot) ||
182
+ path.join(appRoot, 'node_modules')
183
+ );
184
+ };
185
+
186
+ const resolveFrameworkInstallRoot = (appRoot: string): string =>
187
+ findVisiblePackageInstall(appRoot, 'proteum') || path.join(resolveAppNodeModulesRoot(appRoot), 'proteum');
188
+
189
+ const resolveFrameworkInstallGraph = (appRoot: string, activeRoot: string): TFrameworkInstallGraph => {
190
+ const installedRoot = findVisiblePackageInstall(appRoot, 'proteum');
191
+
192
+ return {
193
+ activeRoot,
194
+ installedRoot,
195
+ appNodeModulesRoot: resolveAppNodeModulesRoot(appRoot),
196
+ frameworkNodeModulesRoot: findVisibleNodeModulesRoot(activeRoot) || path.join(activeRoot, 'node_modules'),
197
+ };
198
+ };
199
+
200
+ const resolvePackageJsonPath = (packageName: string, searchPaths: string[]): string =>
201
+ require.resolve(`${packageName}/package.json`, { paths: searchPaths });
202
+
72
203
  const filenameToImportName = (value: string) =>
73
204
  normalizeImportPath(value).replace(/[^A-Za-z0-9_]+/g, '_');
74
205
 
206
+ export const resolveFrameworkInstallInfo = ({
207
+ appRoot,
208
+ framework,
209
+ }: {
210
+ appRoot: string;
211
+ framework: TFrameworkInstallGraph;
212
+ }): TFrameworkInstallInfo => {
213
+ const dependencySpec = readPackageDependencySpec(appRoot, 'proteum');
214
+ const installedRoot = framework.installedRoot ? path.resolve(framework.installedRoot) : undefined;
215
+ const normalizedActiveRoot = normalizeImportPath(safeRealpath(framework.activeRoot));
216
+ const installedRootIsSymlink =
217
+ installedRoot !== undefined &&
218
+ (() => {
219
+ try {
220
+ return fs.lstatSync(installedRoot).isSymbolicLink();
221
+ } catch {
222
+ return false;
223
+ }
224
+ })();
225
+
226
+ if (dependencySpec?.startsWith('file:') || dependencySpec?.startsWith('link:')) {
227
+ return {
228
+ mode: 'path',
229
+ summary: `path (${dependencySpec})`,
230
+ dependencySpec,
231
+ };
232
+ }
233
+
234
+ if (dependencySpec?.startsWith('workspace:')) {
235
+ return {
236
+ mode: 'workspace',
237
+ summary: `workspace (${dependencySpec})`,
238
+ dependencySpec,
239
+ };
240
+ }
241
+
242
+ if (installedRootIsSymlink) {
243
+ return {
244
+ mode: 'npm-link',
245
+ summary: 'npm link',
246
+ dependencySpec,
247
+ };
248
+ }
249
+
250
+ if (dependencySpec) {
251
+ return {
252
+ mode: 'npm',
253
+ summary: `npm (${dependencySpec})`,
254
+ dependencySpec,
255
+ };
256
+ }
257
+
258
+ if (!normalizedActiveRoot.includes('/node_modules/')) {
259
+ return {
260
+ mode: 'checkout',
261
+ summary: 'local checkout',
262
+ };
263
+ }
264
+
265
+ return {
266
+ mode: 'global',
267
+ summary: 'global install',
268
+ };
269
+ };
270
+
75
271
  /*----------------------------------
76
272
  - LIB
77
273
  ----------------------------------*/
@@ -79,13 +275,15 @@ export default class Paths {
79
275
  /*----------------------------------
80
276
  - LISTE
81
277
  ----------------------------------*/
82
-
83
- public constructor(
84
- public appRoot: string,
85
- public coreRoot = resolveCoreRoot(appRoot),
86
- ) {}
87
-
88
- public core = { cli: path.resolve(__dirname, '.'), root: this.coreRoot, pages: this.coreRoot + '/client/pages' };
278
+ public coreRoot: string;
279
+ public framework: TFrameworkInstallGraph;
280
+ public core: { cli: string; root: string; pages: string };
281
+
282
+ public constructor(public appRoot: string, coreRoot = resolveCoreRoot(appRoot)) {
283
+ this.coreRoot = coreRoot;
284
+ this.framework = resolveFrameworkInstallGraph(appRoot, coreRoot);
285
+ this.core = { cli: path.resolve(__dirname, '.'), root: this.coreRoot, pages: this.coreRoot + '/client/pages' };
286
+ }
89
287
 
90
288
  /*----------------------------------
91
289
  - EXTRACTION
@@ -171,4 +369,137 @@ export default class Paths {
171
369
  //console.log('Applying Aliases ...', aliases.forModuleAlias());
172
370
  moduleAlias.addAliases(aliases.forModuleAlias());
173
371
  }
372
+
373
+ public getFrameworkRoots(): string[] {
374
+ return [
375
+ this.framework.activeRoot,
376
+ ...(this.framework.installedRoot ? [this.framework.installedRoot] : []),
377
+ ...this.getVisiblePackageInstallRoots('proteum'),
378
+ ].filter((rootPath, index, list) => list.indexOf(rootPath) === index && fs.existsSync(rootPath));
379
+ }
380
+
381
+ public getVisibleNodeModulesRootsForPath(startPath: string): string[] {
382
+ return findVisibleNodeModulesRoots(startPath);
383
+ }
384
+
385
+ public getVisiblePackageInstallRoots(packageName: string, startPath = this.appRoot): string[] {
386
+ return findVisiblePackageInstalls(startPath, packageName);
387
+ }
388
+
389
+ public getFrameworkInstallRoot(): string {
390
+ return this.getFrameworkInstallRootForAppRoot(this.appRoot);
391
+ }
392
+
393
+ public getFrameworkInstallRootForAppRoot(appRoot: string): string {
394
+ return resolveFrameworkInstallRoot(appRoot);
395
+ }
396
+
397
+ public getAppNodeModulesRootForAppRoot(appRoot: string): string {
398
+ return resolveAppNodeModulesRoot(appRoot);
399
+ }
400
+
401
+ public relativePathFromFile(targetFile: string, absolutePath: string): string {
402
+ return this.relativePathFromDirectory(path.dirname(targetFile), absolutePath);
403
+ }
404
+
405
+ public relativePathFromDirectory(targetDirectory: string, absolutePath: string): string {
406
+ const relativePath = normalizeImportPath(path.relative(targetDirectory, absolutePath));
407
+ if (relativePath === '') return '.';
408
+ if (!relativePath.startsWith('.')) return `./${relativePath}`;
409
+ return relativePath;
410
+ }
411
+
412
+ public relativeFrameworkPathFrom(targetFile: string, ...segments: string[]): string {
413
+ return this.relativePathFromFile(targetFile, path.join(this.getFrameworkInstallRoot(), ...segments));
414
+ }
415
+
416
+ public relativeAppNodeModulesPathFrom(targetFile: string, ...segments: string[]): string {
417
+ return this.relativePathFromFile(targetFile, path.join(this.framework.appNodeModulesRoot, ...segments));
418
+ }
419
+
420
+ public relativeFrameworkPathFromDirectory(targetDirectory: string, ...segments: string[]): string {
421
+ return this.relativePathFromDirectory(targetDirectory, path.join(this.getFrameworkInstallRoot(), ...segments));
422
+ }
423
+
424
+ public relativeAppNodeModulesPathFromDirectory(targetDirectory: string, ...segments: string[]): string {
425
+ return this.relativePathFromDirectory(targetDirectory, path.join(this.framework.appNodeModulesRoot, ...segments));
426
+ }
427
+
428
+ public relativeFrameworkTsconfigPathFrom(targetFile: string): string {
429
+ return this.relativeFrameworkPathFrom(targetFile, 'tsconfig.common.json');
430
+ }
431
+
432
+ public relativeFrameworkPathForAppRoot(appRoot: string, targetFile: string, ...segments: string[]): string {
433
+ return this.relativePathFromFile(targetFile, path.join(this.getFrameworkInstallRootForAppRoot(appRoot), ...segments));
434
+ }
435
+
436
+ public relativeFrameworkPathFromDirectoryForAppRoot(
437
+ appRoot: string,
438
+ targetDirectory: string,
439
+ ...segments: string[]
440
+ ): string {
441
+ return this.relativePathFromDirectory(
442
+ targetDirectory,
443
+ path.join(this.getFrameworkInstallRootForAppRoot(appRoot), ...segments),
444
+ );
445
+ }
446
+
447
+ public relativeAppNodeModulesPathFromDirectoryForAppRoot(
448
+ appRoot: string,
449
+ targetDirectory: string,
450
+ ...segments: string[]
451
+ ): string {
452
+ return this.relativePathFromDirectory(
453
+ targetDirectory,
454
+ path.join(this.getAppNodeModulesRootForAppRoot(appRoot), ...segments),
455
+ );
456
+ }
457
+
458
+ public resolvePackageRoot(packageName: string, { preferApp = true }: { preferApp?: boolean } = {}): string {
459
+ const installedRoot = this.framework.installedRoot;
460
+ const searchPaths = (preferApp
461
+ ? [this.appRoot, ...(installedRoot ? [installedRoot] : []), this.framework.activeRoot]
462
+ : [this.framework.activeRoot, ...(installedRoot ? [installedRoot] : []), this.appRoot]
463
+ ).filter((searchPath, index, list) => list.indexOf(searchPath) === index);
464
+ return path.dirname(resolvePackageJsonPath(packageName, searchPaths));
465
+ }
466
+
467
+ public resolveRequest(request: string, { preferApp = true }: { preferApp?: boolean } = {}): string {
468
+ const installedRoot = this.framework.installedRoot;
469
+ const searchPaths = (preferApp
470
+ ? [this.appRoot, ...(installedRoot ? [installedRoot] : []), this.framework.activeRoot]
471
+ : [this.framework.activeRoot, ...(installedRoot ? [installedRoot] : []), this.appRoot]
472
+ ).filter((searchPath, index, list) => list.indexOf(searchPath) === index);
473
+ return require.resolve(request, { paths: searchPaths });
474
+ }
475
+
476
+ public resolveBinary(
477
+ packageName: string,
478
+ binName = packageName,
479
+ { preferApp = true }: { preferApp?: boolean } = {},
480
+ ): TResolvedPackageBinary {
481
+ const packageRoot = this.resolvePackageRoot(packageName, { preferApp });
482
+ const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')) as {
483
+ bin?: string | Record<string, string>;
484
+ };
485
+ const binField = packageJson.bin;
486
+ const relativeBinPath =
487
+ typeof binField === 'string'
488
+ ? binField
489
+ : binField?.[binName] || binField?.[packageName];
490
+
491
+ if (!relativeBinPath) {
492
+ throw new Error(`Unable to resolve binary "${binName}" from package "${packageName}".`);
493
+ }
494
+
495
+ const binPath = path.resolve(packageRoot, relativeBinPath);
496
+
497
+ return {
498
+ packageName,
499
+ packageRoot,
500
+ binPath,
501
+ command: process.execPath,
502
+ args: [binPath],
503
+ };
504
+ }
174
505
  }
@@ -1,15 +1,18 @@
1
- const React = require('react') as typeof import('react');
2
-
1
+ import type { TServerReadyConnectedProject } from '../../common/dev/serverHotReload';
3
2
  import { renderRows } from './layout';
4
- import { renderInk } from './ink';
3
+ import { CliReact, renderInk } from './ink';
5
4
  import { renderWelcomePanel } from './welcome';
6
5
 
6
+ const formatConnectedProjectLabel = (connectedProject: TServerReadyConnectedProject) =>
7
+ `${connectedProject.namespace} -> ${connectedProject.name}`;
8
+
7
9
  export const renderDevSession = async ({
8
10
  appName,
9
11
  appRoot,
10
12
  routerPort,
11
13
  devEventPort,
12
14
  connectedProjects,
15
+ proteumInstallSummary,
13
16
  proteumVersion,
14
17
  }: {
15
18
  appName: string;
@@ -17,10 +20,12 @@ export const renderDevSession = async ({
17
20
  routerPort: number;
18
21
  devEventPort: number;
19
22
  connectedProjects?: Array<{ namespace: string; urlInternal: string }>;
23
+ proteumInstallSummary?: string;
20
24
  proteumVersion: string;
21
25
  }) =>
22
26
  [
23
27
  await renderWelcomePanel({
28
+ installSummary: proteumInstallSummary,
24
29
  version: proteumVersion,
25
30
  tagline: 'Agent-first SSR compiler and server loop.',
26
31
  }),
@@ -51,15 +56,16 @@ export const renderServerReadyBanner = async ({
51
56
  appName,
52
57
  publicUrl,
53
58
  routerPort,
54
- connectedProjectsCount,
59
+ connectedProjects,
55
60
  }: {
56
61
  appName: string;
57
62
  publicUrl: string;
58
63
  routerPort: number;
59
- connectedProjectsCount?: number;
64
+ connectedProjects?: TServerReadyConnectedProject[];
60
65
  }) =>
61
66
  renderInk(({ Box, Text }) => {
62
- const createElement = React.createElement;
67
+ const createElement = CliReact.createElement;
68
+ const verifiedConnectedProjects = connectedProjects || [];
63
69
 
64
70
  return createElement(
65
71
  Box,
@@ -68,13 +74,20 @@ export const renderServerReadyBanner = async ({
68
74
  createElement(Text, { bold: true, color: 'green' }, appName),
69
75
  createElement(Text, { bold: true }, publicUrl),
70
76
  createElement(Text, { dimColor: true }, 'SSR server is listening for requests and hot reloads.'),
71
- connectedProjectsCount
77
+ verifiedConnectedProjects.length > 0
72
78
  ? createElement(
73
79
  Text,
74
80
  { dimColor: true },
75
- `Connected projects: ${connectedProjectsCount}`,
81
+ `Connected apps: ${verifiedConnectedProjects.map((connectedProject) => formatConnectedProjectLabel(connectedProject)).join(', ')}`,
76
82
  )
77
83
  : null,
84
+ ...verifiedConnectedProjects.map((connectedProject) =>
85
+ createElement(
86
+ Text,
87
+ { key: `connected-ping-${connectedProject.namespace}`, dimColor: true },
88
+ `Ping OK (/ping): ${formatConnectedProjectLabel(connectedProject)}`,
89
+ ),
90
+ ),
78
91
  createElement(Text, { dimColor: true }, `Diagnose /: proteum diagnose / --port ${routerPort}`),
79
92
  createElement(Text, { dimColor: true }, `Perf top: proteum perf top --port ${routerPort}`),
80
93
  createElement(Text, { dimColor: true }, `Trace latest: proteum trace latest --port ${routerPort}`),
@@ -83,7 +96,7 @@ export const renderServerReadyBanner = async ({
83
96
 
84
97
  export const renderDevShutdownBanner = async () =>
85
98
  renderInk(({ Box, Text }) => {
86
- const createElement = React.createElement;
99
+ const createElement = CliReact.createElement;
87
100
 
88
101
  return createElement(
89
102
  Box,
@@ -1,4 +1,4 @@
1
- const React = require('react') as typeof import('react');
1
+ import { createRequire } from 'module';
2
2
 
3
3
  import { importEsm } from '../runtime/importEsm';
4
4
  import { getTerminalWidth } from './layout';
@@ -13,6 +13,9 @@ type TInkRuntime = {
13
13
  StatusMessage: TInkUiModule['StatusMessage'];
14
14
  };
15
15
 
16
+ // Keep the CLI renderer on the exact React instance Ink resolved for this install shape.
17
+ const CliReact = createRequire(require.resolve('ink'))('react') as typeof import('react');
18
+
16
19
  let inkRuntimePromise: Promise<TInkRuntime> | undefined;
17
20
 
18
21
  const loadInkRuntime = () => {
@@ -41,7 +44,7 @@ export const renderInk = async (
41
44
 
42
45
  export const renderTitle = async (title: string, subtitle?: string) =>
43
46
  renderInk(({ Box, Text }) => {
44
- const createElement = React.createElement;
47
+ const createElement = CliReact.createElement;
45
48
 
46
49
  return createElement(
47
50
  Box,
@@ -52,18 +55,20 @@ export const renderTitle = async (title: string, subtitle?: string) =>
52
55
  });
53
56
 
54
57
  export const renderSection = async (title: string, body: string) => {
55
- const heading = await renderInk(({ Text }) => React.createElement(Text, { bold: true }, title));
58
+ const heading = await renderInk(({ Text }) => CliReact.createElement(Text, { bold: true }, title));
56
59
  return `${heading}\n${body}`;
57
60
  };
58
61
 
59
62
  export const renderStep = async (label: string, message: string) =>
60
- renderInk(({ Text }) => React.createElement(Text, { color: 'cyan' }, `${label} ${message}`));
63
+ renderInk(({ Text }) => CliReact.createElement(Text, { color: 'cyan' }, `${label} ${message}`));
61
64
 
62
65
  const renderStatusMessage = async (variant: 'success' | 'warning' | 'error', message: string) =>
63
- renderInk(({ StatusMessage }) => React.createElement(StatusMessage, { variant }, message));
66
+ renderInk(({ StatusMessage }) => CliReact.createElement(StatusMessage, { variant }, message));
64
67
 
65
68
  export const renderSuccess = (message: string) => renderStatusMessage('success', message);
66
69
 
67
70
  export const renderWarning = (message: string) => renderStatusMessage('warning', message);
68
71
 
69
72
  export const renderDanger = (message: string) => renderStatusMessage('error', message);
73
+
74
+ export { CliReact };
@@ -1,7 +1,5 @@
1
- const React = require('react') as typeof import('react');
2
-
3
1
  import { renderRows } from './layout';
4
- import { renderInk } from './ink';
2
+ import { CliReact, renderInk } from './ink';
5
3
 
6
4
  const ProteumWordmark = [
7
5
  String.raw` ____ ____ ___ _____ _____ _ _ __ __`,
@@ -18,14 +16,16 @@ export const clearInteractiveConsole = () => {
18
16
  };
19
17
 
20
18
  export const renderWelcomePanel = async ({
19
+ installSummary,
21
20
  version,
22
21
  tagline,
23
22
  }: {
23
+ installSummary?: string;
24
24
  version: string;
25
25
  tagline: string;
26
26
  }) =>
27
27
  renderInk(({ Box, Text }) => {
28
- const createElement = React.createElement;
28
+ const createElement = CliReact.createElement;
29
29
  const wordmark = ProteumWordmark.map((line) =>
30
30
  createElement(Text, { key: line, bold: true, color: 'blue' }, line),
31
31
  );
@@ -37,19 +37,23 @@ export const renderWelcomePanel = async ({
37
37
  createElement(Text, { bold: true, backgroundColor: 'blue', color: 'white' }, ' WELCOME TO '),
38
38
  createElement(Box, { flexDirection: 'column' }, ...wordmark),
39
39
  versionLabel ? createElement(Text, { bold: true, color: 'blue' }, versionLabel) : null,
40
+ installSummary ? createElement(Text, { dimColor: true }, `Installed via ${installSummary}`) : null,
40
41
  createElement(Text, { dimColor: true }, tagline),
41
42
  );
42
43
  });
43
44
 
44
45
  export const renderCliWelcomeBanner = async ({
45
46
  command,
47
+ installSummary,
46
48
  version,
47
49
  }: {
48
50
  command: string;
51
+ installSummary?: string;
49
52
  version: string;
50
53
  }) =>
51
54
  [
52
55
  await renderWelcomePanel({
56
+ installSummary,
53
57
  version,
54
58
  tagline: 'Explicit SSR / SEO / TypeScript framework for agent-friendly apps.',
55
59
  }),
@@ -26,6 +26,7 @@ import {
26
26
  createServiceConfigTemplate,
27
27
  createServiceTemplate,
28
28
  } from './templates';
29
+ import type { TTsconfigTemplatePaths } from './templates';
29
30
  import type { TScaffoldFilePlan, TScaffoldInitConfig, TScaffoldKind, TScaffoldResult } from './types';
30
31
 
31
32
  type TCreatePlan = {
@@ -602,7 +603,29 @@ const assertInitTarget = ({ appRoot }: { appRoot: string }) => {
602
603
  );
603
604
  };
604
605
 
605
- const createInitFilePlans = (config: TScaffoldInitConfig): TScaffoldFilePlan[] => [
606
+ const createAppTsconfigTemplatePaths = ({
607
+ appRoot,
608
+ side,
609
+ }: {
610
+ appRoot: string;
611
+ side: 'client' | 'server';
612
+ }): TTsconfigTemplatePaths => {
613
+ const tsconfigFilepath = path.join(appRoot, side, 'tsconfig.json');
614
+
615
+ return {
616
+ frameworkTsconfig: cli.paths.relativeFrameworkPathForAppRoot(appRoot, tsconfigFilepath, 'tsconfig.common.json'),
617
+ frameworkClient: cli.paths.relativeFrameworkPathFromDirectoryForAppRoot(appRoot, appRoot, 'client', '*'),
618
+ frameworkCommon: cli.paths.relativeFrameworkPathFromDirectoryForAppRoot(appRoot, appRoot, 'common', '*'),
619
+ frameworkServer: cli.paths.relativeFrameworkPathFromDirectoryForAppRoot(appRoot, appRoot, 'server', '*'),
620
+ frameworkTypesGlobal: cli.paths.relativeFrameworkPathForAppRoot(appRoot, tsconfigFilepath, 'types', 'global'),
621
+ preactCompat: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'compat'),
622
+ preactCompatClient: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'compat', 'client'),
623
+ preactTestUtils: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'test-utils'),
624
+ preactJsxRuntime: cli.paths.relativeAppNodeModulesPathFromDirectoryForAppRoot(appRoot, appRoot, 'preact', 'jsx-runtime'),
625
+ };
626
+ };
627
+
628
+ const createInitFilePlans = ({ appRoot, config }: { appRoot: string; config: TScaffoldInitConfig }): TScaffoldFilePlan[] => [
606
629
  {
607
630
  relativePath: 'package.json',
608
631
  content: createPackageJsonTemplate({
@@ -643,11 +666,11 @@ const createInitFilePlans = (config: TScaffoldInitConfig): TScaffoldFilePlan[] =
643
666
  },
644
667
  {
645
668
  relativePath: path.join('client', 'tsconfig.json'),
646
- content: createClientTsconfigTemplate(),
669
+ content: createClientTsconfigTemplate(createAppTsconfigTemplatePaths({ appRoot, side: 'client' })),
647
670
  },
648
671
  {
649
672
  relativePath: path.join('server', 'tsconfig.json'),
650
- content: createServerTsconfigTemplate(),
673
+ content: createServerTsconfigTemplate(createAppTsconfigTemplatePaths({ appRoot, side: 'server' })),
651
674
  },
652
675
  {
653
676
  relativePath: path.join('server', 'config', 'app.ts'),
@@ -675,7 +698,7 @@ export const runInitScaffold = async () => {
675
698
  assertInitTarget({ appRoot });
676
699
 
677
700
  const result = createEmptyResult({ dryRun: isDryRun() });
678
- const filePlans = createInitFilePlans(config);
701
+ const filePlans = createInitFilePlans({ appRoot, config });
679
702
 
680
703
  maybeWriteFilePlans({ rootDir: appRoot, filePlans, result });
681
704