@olonjs/cli 3.0.154 → 3.0.155
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/assets/src_tenant_alpha.sh +3 -3
- package/assets/templates/alpha/src_tenant.sh +3 -3
- package/assets/templates/next/manifest.json +5 -0
- package/assets/templates/next/src_tenant.sh +4174 -0
- package/package.json +2 -1
- package/src/index.js +78 -25
- package/src/selectWithArrows.js +71 -0
- package/src/templateChoice.js +29 -0
- package/src/templateChoice.test.js +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olonjs/cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.155",
|
|
4
4
|
"description": "The Sovereign CLI Engine for OlonJS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "tsc -p .",
|
|
31
|
+
"test": "node --test src/**/*.test.js",
|
|
31
32
|
"check:templates": "node ../../scripts/check-cli-templates.mjs"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,12 @@ import { execa } from 'execa';
|
|
|
7
7
|
import ora from 'ora';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import os from 'os';
|
|
10
|
+
import {
|
|
11
|
+
resolveTemplateId,
|
|
12
|
+
shouldPromptForTemplate,
|
|
13
|
+
UI_TEMPLATE_CHOICES,
|
|
14
|
+
} from './templateChoice.js';
|
|
15
|
+
import { selectWithArrows } from './selectWithArrows.js';
|
|
10
16
|
|
|
11
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
18
|
const __dirname = path.dirname(__filename);
|
|
@@ -86,11 +92,34 @@ function resolveTemplateScriptPath(templateName) {
|
|
|
86
92
|
return templatePath;
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
async function resolveDnaTemplateId(templateOption) {
|
|
96
|
+
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
97
|
+
if (shouldPromptForTemplate({ templateOption, isTTY })) {
|
|
98
|
+
const dnaId = await selectWithArrows(
|
|
99
|
+
chalk.bold('Select stack:'),
|
|
100
|
+
UI_TEMPLATE_CHOICES.map((c) => ({ label: c.label, value: c.dnaId })),
|
|
101
|
+
);
|
|
102
|
+
return dnaId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (templateOption == null || String(templateOption).trim() === '') {
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Non-interactive mode requires --template next|vite|alpha (for agents/CI).',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const dnaId = resolveTemplateId(templateOption);
|
|
112
|
+
if (!dnaId) {
|
|
113
|
+
throw new Error(`Unknown template "${templateOption}". Use next | vite | alpha.`);
|
|
114
|
+
}
|
|
115
|
+
return dnaId;
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
program
|
|
90
119
|
.command('new')
|
|
91
120
|
.argument('<type>', 'Type of artifact (tenant)')
|
|
92
121
|
.argument('<name>', 'Name of the new tenant')
|
|
93
|
-
.option('--template <name>', 'Template
|
|
122
|
+
.option('--template <name>', 'Template for agents/CI: next | vite | alpha (skips interactive picker)')
|
|
94
123
|
.option('--script <path>', 'Override default deterministic script path')
|
|
95
124
|
.action(async (type, name, options) => {
|
|
96
125
|
if (type !== 'tenant') {
|
|
@@ -100,46 +129,70 @@ program
|
|
|
100
129
|
|
|
101
130
|
const targetDir = path.join(process.cwd(), name);
|
|
102
131
|
const availableTemplates = getAvailableTemplates();
|
|
103
|
-
|
|
132
|
+
|
|
133
|
+
let dnaId;
|
|
134
|
+
try {
|
|
135
|
+
dnaId = await resolveDnaTemplateId(options.template);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
104
141
|
const scriptPath = options.script
|
|
105
142
|
? path.resolve(process.cwd(), options.script)
|
|
106
|
-
: resolveTemplateScriptPath(
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
143
|
+
: resolveTemplateScriptPath(dnaId);
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
!options.script &&
|
|
147
|
+
!availableTemplates.includes(dnaId) &&
|
|
148
|
+
!(dnaId === 'alpha' && fs.existsSync(LEGACY_ALPHA_DNA_PATH))
|
|
149
|
+
) {
|
|
150
|
+
console.log(chalk.red(`Error: Unknown template "${dnaId}".`));
|
|
151
|
+
console.log(
|
|
152
|
+
chalk.yellow(
|
|
153
|
+
`Available templates: ${availableTemplates.length ? availableTemplates.join(', ') : '(none found)'}`,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
111
156
|
return;
|
|
112
157
|
}
|
|
113
158
|
|
|
114
159
|
if (!fs.existsSync(scriptPath)) {
|
|
115
160
|
console.log(chalk.red(`Error: DNA script not found at ${scriptPath}`));
|
|
116
|
-
console.log(chalk.yellow(`Debug info: template=${
|
|
161
|
+
console.log(chalk.yellow(`Debug info: template=${dnaId}, assets=${CLI_ASSETS_DIR}`));
|
|
117
162
|
return;
|
|
118
163
|
}
|
|
119
164
|
|
|
120
|
-
|
|
165
|
+
const stackLabel = dnaId === 'next' ? 'next' : 'vite';
|
|
166
|
+
console.log(chalk.blue.bold(`\nProjecting Sovereign Tenant: ${name} (stack: ${stackLabel}, dna: ${dnaId})\n`));
|
|
121
167
|
const spinner = ora();
|
|
168
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
122
169
|
|
|
123
170
|
try {
|
|
124
|
-
spinner.start('Setting up environment (Vite + TS)...');
|
|
125
171
|
await fs.ensureDir(targetDir);
|
|
126
172
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
173
|
+
if (dnaId === 'next') {
|
|
174
|
+
spinner.start('Preparing Next App Router workspace...');
|
|
175
|
+
spinner.succeed('Workspace ready.');
|
|
176
|
+
} else {
|
|
177
|
+
spinner.start('Setting up environment (Vite + TS)...');
|
|
178
|
+
await execa(npmCmd, ['create', 'vite@latest', '.', '--', '--template', 'react-ts'], {
|
|
179
|
+
cwd: targetDir,
|
|
180
|
+
});
|
|
181
|
+
spinner.succeed('Environment scaffolded.');
|
|
182
|
+
|
|
183
|
+
spinner.start('Wiping default boilerplate...');
|
|
184
|
+
await fs.emptyDir(path.join(targetDir, 'src'));
|
|
185
|
+
const junk = ['App.css', 'App.tsx', 'main.tsx', 'vite-env.d.ts', 'favicon.ico', 'index.html'];
|
|
186
|
+
for (const file of junk) {
|
|
187
|
+
await fs.remove(path.join(targetDir, file)).catch(() => {});
|
|
188
|
+
await fs.remove(path.join(targetDir, 'src', file)).catch(() => {});
|
|
189
|
+
}
|
|
190
|
+
spinner.succeed('Clean slate achieved.');
|
|
130
191
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for (const file of junk) {
|
|
135
|
-
await fs.remove(path.join(targetDir, file)).catch(() => {});
|
|
136
|
-
await fs.remove(path.join(targetDir, 'src', file)).catch(() => {});
|
|
192
|
+
spinner.start('Injecting Sovereign Configurations...');
|
|
193
|
+
await injectInfraFiles(targetDir, name);
|
|
194
|
+
spinner.succeed('Infrastructure configured.');
|
|
137
195
|
}
|
|
138
|
-
spinner.succeed('Clean slate achieved.');
|
|
139
|
-
|
|
140
|
-
spinner.start('Injecting Sovereign Configurations...');
|
|
141
|
-
await injectInfraFiles(targetDir, name);
|
|
142
|
-
spinner.succeed('Infrastructure configured.');
|
|
143
196
|
|
|
144
197
|
spinner.start('Executing deterministic src projection...');
|
|
145
198
|
await processScriptInNode(scriptPath, targetDir);
|
|
@@ -153,7 +206,7 @@ program
|
|
|
153
206
|
console.log(` ${chalk.cyan(`cd ${name}`)}`);
|
|
154
207
|
console.log(` ${chalk.cyan('npm run dev')} <- Start development`);
|
|
155
208
|
console.log(` ${chalk.cyan('npm run build')} <- Validate Green Build`);
|
|
156
|
-
console.log(`\
|
|
209
|
+
console.log(`\nStack: ${stackLabel} · DNA: ${dnaId}\n`);
|
|
157
210
|
} catch (error) {
|
|
158
211
|
spinner.fail(chalk.red('Projection failed.'));
|
|
159
212
|
console.error(error);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal arrow-key select (no extra deps).
|
|
5
|
+
* @param {string} message
|
|
6
|
+
* @param {Array<{ label: string, value: string }>} choices
|
|
7
|
+
* @returns {Promise<string>} selected value
|
|
8
|
+
*/
|
|
9
|
+
export async function selectWithArrows(message, choices) {
|
|
10
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
11
|
+
throw new Error('Interactive template selection requires a TTY. Pass --template next|vite|alpha.');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let index = 0;
|
|
15
|
+
|
|
16
|
+
const render = () => {
|
|
17
|
+
const lines = choices.map((choice, i) => {
|
|
18
|
+
const mark = i === index ? '>' : ' ';
|
|
19
|
+
return ` ${mark} ${choice.label}`;
|
|
20
|
+
});
|
|
21
|
+
process.stdout.write(`\x1B[?25l${message}\n${lines.join('\n')}\n`);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const clear = () => {
|
|
25
|
+
const lineCount = choices.length + 1;
|
|
26
|
+
process.stdout.write(`\x1B[${lineCount}A\x1B[0J`);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
render();
|
|
30
|
+
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
readline.emitKeypressEvents(process.stdin);
|
|
33
|
+
process.stdin.setRawMode(true);
|
|
34
|
+
process.stdin.resume();
|
|
35
|
+
|
|
36
|
+
const onKey = (_str, key) => {
|
|
37
|
+
if (!key) return;
|
|
38
|
+
if (key.ctrl && key.name === 'c') {
|
|
39
|
+
cleanup();
|
|
40
|
+
reject(new Error('Cancelled'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
44
|
+
index = (index - 1 + choices.length) % choices.length;
|
|
45
|
+
clear();
|
|
46
|
+
render();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
50
|
+
index = (index + 1) % choices.length;
|
|
51
|
+
clear();
|
|
52
|
+
render();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (key.name === 'return') {
|
|
56
|
+
cleanup();
|
|
57
|
+
process.stdout.write(`\x1B[?25h`);
|
|
58
|
+
resolve(choices[index].value);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function cleanup() {
|
|
63
|
+
process.stdin.setRawMode(false);
|
|
64
|
+
process.stdin.pause();
|
|
65
|
+
process.stdin.off('keypress', onKey);
|
|
66
|
+
process.stdout.write('\x1B[?25h');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
process.stdin.on('keypress', onKey);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template selection for `olonjs new tenant`.
|
|
3
|
+
* UI labels: next | vite → DNA ids: next | alpha
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const UI_TEMPLATE_CHOICES = [
|
|
7
|
+
{ label: 'next', dnaId: 'next' },
|
|
8
|
+
{ label: 'vite', dnaId: 'alpha' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** @param {string | undefined | null} raw */
|
|
12
|
+
export function resolveTemplateId(raw) {
|
|
13
|
+
const v = String(raw ?? '')
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase();
|
|
16
|
+
if (!v) return null;
|
|
17
|
+
if (v === 'next') return 'next';
|
|
18
|
+
if (v === 'vite' || v === 'alpha') return 'alpha';
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Prompt when humans run without --template on a TTY.
|
|
24
|
+
* Agents/CI must pass --template (non-TTY without flag → no prompt).
|
|
25
|
+
*/
|
|
26
|
+
export function shouldPromptForTemplate({ templateOption, isTTY }) {
|
|
27
|
+
if (templateOption != null && String(templateOption).trim() !== '') return false;
|
|
28
|
+
return Boolean(isTTY);
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
resolveTemplateId,
|
|
5
|
+
shouldPromptForTemplate,
|
|
6
|
+
UI_TEMPLATE_CHOICES,
|
|
7
|
+
} from './templateChoice.js';
|
|
8
|
+
|
|
9
|
+
describe('UI_TEMPLATE_CHOICES', () => {
|
|
10
|
+
it('exposes next and vite labels for the TUI', () => {
|
|
11
|
+
assert.deepEqual(
|
|
12
|
+
UI_TEMPLATE_CHOICES.map((c) => c.label),
|
|
13
|
+
['next', 'vite'],
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('resolveTemplateId', () => {
|
|
19
|
+
it('maps next → next and vite/alpha → alpha', () => {
|
|
20
|
+
assert.equal(resolveTemplateId('next'), 'next');
|
|
21
|
+
assert.equal(resolveTemplateId('vite'), 'alpha');
|
|
22
|
+
assert.equal(resolveTemplateId('alpha'), 'alpha');
|
|
23
|
+
assert.equal(resolveTemplateId('NEXT'), 'next');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for unknown values', () => {
|
|
27
|
+
assert.equal(resolveTemplateId('spa'), null);
|
|
28
|
+
assert.equal(resolveTemplateId(''), null);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('shouldPromptForTemplate', () => {
|
|
33
|
+
it('prompts only when flag absent and stdin is a TTY', () => {
|
|
34
|
+
assert.equal(shouldPromptForTemplate({ templateOption: undefined, isTTY: true }), true);
|
|
35
|
+
assert.equal(shouldPromptForTemplate({ templateOption: 'next', isTTY: true }), false);
|
|
36
|
+
assert.equal(shouldPromptForTemplate({ templateOption: undefined, isTTY: false }), false);
|
|
37
|
+
});
|
|
38
|
+
});
|