@emeryld/manager 0.5.0 → 0.5.1
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/dist/create-package/index.js +37 -27
- package/dist/create-package/variants/docker.js +82 -37
- package/dist/menu.js +62 -4
- package/dist/prompts.js +21 -15
- package/dist/publish.js +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { stdin as input } from 'node:process';
|
|
5
5
|
import { askLine, promptSingleKey } from '../prompts.js';
|
|
6
6
|
import { colors, logGlobal } from '../utils/log.js';
|
|
7
|
+
import { runHelperCli } from '../helper-cli.js';
|
|
7
8
|
import { loadPackages } from '../packages.js';
|
|
8
9
|
import { SCRIPT_DESCRIPTIONS, workspaceRoot, ensureWorkspaceToolingFiles, } from './shared.js';
|
|
9
10
|
import { clientVariant } from './variants/client.js';
|
|
@@ -25,6 +26,36 @@ function derivePackageName(targetDir) {
|
|
|
25
26
|
const base = path.basename(targetDir) || 'rrr-package';
|
|
26
27
|
return base;
|
|
27
28
|
}
|
|
29
|
+
async function promptForContractNameWithHelper(title, options) {
|
|
30
|
+
let selection;
|
|
31
|
+
const scripts = options.map((opt) => ({
|
|
32
|
+
name: opt.label,
|
|
33
|
+
description: opt.meta ?? '',
|
|
34
|
+
emoji: '📦',
|
|
35
|
+
handler: () => {
|
|
36
|
+
selection = opt.value;
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
scripts.push({
|
|
40
|
+
name: 'Enter manually',
|
|
41
|
+
emoji: '⌨️',
|
|
42
|
+
description: 'Type a contract package name',
|
|
43
|
+
handler: async () => {
|
|
44
|
+
const manual = await askLine('Contract package name (e.g. @scope/contract): ');
|
|
45
|
+
selection = manual.trim() || undefined;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
scripts.push({
|
|
49
|
+
name: 'Skip (no contract)',
|
|
50
|
+
emoji: '⏭️',
|
|
51
|
+
description: 'Continue without a contract dependency',
|
|
52
|
+
handler: () => {
|
|
53
|
+
selection = undefined;
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await runHelperCli({ title, scripts, argv: [] });
|
|
57
|
+
return selection;
|
|
58
|
+
}
|
|
28
59
|
async function promptForContractName() {
|
|
29
60
|
let packages = [];
|
|
30
61
|
try {
|
|
@@ -34,39 +65,18 @@ async function promptForContractName() {
|
|
|
34
65
|
const message = error instanceof Error ? error.message : 'unknown error discovering packages';
|
|
35
66
|
logGlobal(`Could not auto-discover packages (${message}).`, colors.yellow);
|
|
36
67
|
}
|
|
37
|
-
const
|
|
68
|
+
const contractOptions = packages
|
|
38
69
|
.filter((pkg) => pkg.relativeDir !== '.')
|
|
39
|
-
.map((pkg
|
|
40
|
-
label: `${pkg.name}${pkg.relativeDir ? colors.dim(` (${pkg.relativeDir})`) : ''}`,
|
|
70
|
+
.map((pkg) => ({
|
|
41
71
|
value: pkg.name,
|
|
42
|
-
|
|
72
|
+
label: pkg.name,
|
|
73
|
+
meta: pkg.relativeDir,
|
|
43
74
|
}));
|
|
44
|
-
if (
|
|
75
|
+
if (contractOptions.length === 0) {
|
|
45
76
|
const manual = await askLine('Contract package name? (Enter to skip, e.g. @scope/contract): ');
|
|
46
77
|
return manual.trim() || undefined;
|
|
47
78
|
}
|
|
48
|
-
|
|
49
|
-
candidates.forEach((opt, idx) => {
|
|
50
|
-
const prefix = colors.cyan(String(idx + 1).padStart(2, ' '));
|
|
51
|
-
console.log(` [${prefix}] ${opt.label}`);
|
|
52
|
-
});
|
|
53
|
-
console.log(` [${colors.cyan('m ')}] Enter manually`);
|
|
54
|
-
console.log(` [${colors.cyan('s ')}] Skip (no contract)`);
|
|
55
|
-
// eslint-disable-next-line no-constant-condition
|
|
56
|
-
while (true) {
|
|
57
|
-
const choice = (await askLine('Choice: ')).trim().toLowerCase();
|
|
58
|
-
if (!choice || choice === 's')
|
|
59
|
-
return undefined;
|
|
60
|
-
if (choice === 'm') {
|
|
61
|
-
const manual = await askLine('Contract package name (e.g. @scope/contract): ');
|
|
62
|
-
return manual.trim() || undefined;
|
|
63
|
-
}
|
|
64
|
-
const idx = Number.parseInt(choice, 10);
|
|
65
|
-
if (Number.isInteger(idx) && idx >= 1 && idx <= candidates.length) {
|
|
66
|
-
return candidates[idx - 1].value;
|
|
67
|
-
}
|
|
68
|
-
console.log(colors.yellow('Enter a number, "m" to type a name, or "s" to skip.'));
|
|
69
|
-
}
|
|
79
|
+
return promptForContractNameWithHelper('Select a contract package (or skip)', contractOptions);
|
|
70
80
|
}
|
|
71
81
|
function resolveVariant(key) {
|
|
72
82
|
if (!key)
|
|
@@ -149,49 +149,21 @@ import { readFile } from 'node:fs/promises'
|
|
|
149
149
|
import path from 'node:path'
|
|
150
150
|
import { fileURLToPath } from 'node:url'
|
|
151
151
|
import { Docker } from 'docker-cli-js'
|
|
152
|
+
import { runHelperCli } from '@emeryld/manager/dist/helper-cli.js'
|
|
152
153
|
|
|
153
154
|
const __filename = fileURLToPath(import.meta.url)
|
|
154
155
|
const __dirname = path.dirname(__filename)
|
|
155
|
-
const
|
|
156
|
+
const pkgRoot = path.join(__dirname, '..')
|
|
157
|
+
const pkgRaw = await readFile(path.join(pkgRoot, 'package.json'), 'utf8')
|
|
156
158
|
const pkg = JSON.parse(pkgRaw) as { name?: string }
|
|
157
159
|
const image = \`\${pkg.name ?? '${pkgName}'}:latest\`
|
|
158
160
|
const container =
|
|
159
|
-
(pkg.name ?? '${pkgName}')
|
|
161
|
+
(pkg.name ?? '${pkgName}')
|
|
162
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
163
|
+
.replace(/^-+|-+$/g, '') || 'rrr-service'
|
|
160
164
|
const port = process.env.PORT ?? '3000'
|
|
161
165
|
const docker = new Docker({ spawnOptions: { stdio: 'inherit' } })
|
|
162
166
|
|
|
163
|
-
async function main() {
|
|
164
|
-
const [command = 'help'] = process.argv.slice(2)
|
|
165
|
-
if (command === 'help') return printHelp()
|
|
166
|
-
if (command === 'build') return docker.command(\`build -t \${image} .\`)
|
|
167
|
-
if (command === 'up') {
|
|
168
|
-
await docker.command(\`build -t \${image} .\`)
|
|
169
|
-
return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
|
|
170
|
-
}
|
|
171
|
-
if (command === 'dev') {
|
|
172
|
-
await docker.command(\`build -t \${image} .\`)
|
|
173
|
-
await docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
|
|
174
|
-
return docker.command(\`logs -f \${container}\`)
|
|
175
|
-
}
|
|
176
|
-
if (command === 'run') {
|
|
177
|
-
return docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
|
|
178
|
-
}
|
|
179
|
-
if (command === 'logs') return docker.command(\`logs -f \${container}\`)
|
|
180
|
-
if (command === 'stop') return docker.command(\`stop \${container}\`)
|
|
181
|
-
if (command === 'clean') {
|
|
182
|
-
await safe(() => docker.command(\`stop \${container}\`))
|
|
183
|
-
await safe(() => docker.command(\`rm -f \${container}\`))
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
if (command === 'reset') {
|
|
187
|
-
await safe(() => docker.command(\`stop \${container}\`))
|
|
188
|
-
await safe(() => docker.command(\`rm -f \${container}\`))
|
|
189
|
-
await safe(() => docker.command(\`rmi -f \${image}\`))
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
return printHelp()
|
|
193
|
-
}
|
|
194
|
-
|
|
195
167
|
async function safe(run: () => Promise<unknown>) {
|
|
196
168
|
try {
|
|
197
169
|
await run()
|
|
@@ -216,9 +188,82 @@ function printHelp() {
|
|
|
216
188
|
)
|
|
217
189
|
}
|
|
218
190
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
process.
|
|
191
|
+
await runHelperCli({
|
|
192
|
+
title: 'Docker helper',
|
|
193
|
+
argv: process.argv.slice(2),
|
|
194
|
+
scripts: [
|
|
195
|
+
{
|
|
196
|
+
name: 'build',
|
|
197
|
+
emoji: '🔨',
|
|
198
|
+
description: 'docker build -t image .',
|
|
199
|
+
handler: () => docker.command(\`build -t \${image} .\`),
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'up',
|
|
203
|
+
emoji: '⬆️',
|
|
204
|
+
description: 'Build + run detached',
|
|
205
|
+
handler: async () => {
|
|
206
|
+
await docker.command(\`build -t \${image} .\`)
|
|
207
|
+
await docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'dev',
|
|
212
|
+
emoji: '🛠️',
|
|
213
|
+
description: 'Build, run, tail logs',
|
|
214
|
+
handler: async () => {
|
|
215
|
+
await docker.command(\`build -t \${image} .\`)
|
|
216
|
+
await docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`)
|
|
217
|
+
await docker.command(\`logs -f \${container}\`)
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'run',
|
|
222
|
+
emoji: '🏃',
|
|
223
|
+
description: 'Run existing image detached',
|
|
224
|
+
handler: () =>
|
|
225
|
+
docker.command(\`run -d --rm -p \${port}:\${port} --name \${container} \${image}\`),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'logs',
|
|
229
|
+
emoji: '📜',
|
|
230
|
+
description: 'Tail docker logs',
|
|
231
|
+
handler: () => docker.command(\`logs -f \${container}\`),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'stop',
|
|
235
|
+
emoji: '🛑',
|
|
236
|
+
description: 'Stop container',
|
|
237
|
+
handler: () => docker.command(\`stop \${container}\`),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'clean',
|
|
241
|
+
emoji: '🧹',
|
|
242
|
+
description: 'Stop + remove container',
|
|
243
|
+
handler: async () => {
|
|
244
|
+
await safe(() => docker.command(\`stop \${container}\`))
|
|
245
|
+
await safe(() => docker.command(\`rm -f \${container}\`))
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'reset',
|
|
250
|
+
emoji: '♻️',
|
|
251
|
+
description: 'Clean container and remove image',
|
|
252
|
+
handler: async () => {
|
|
253
|
+
await safe(() => docker.command(\`stop \${container}\`))
|
|
254
|
+
await safe(() => docker.command(\`rm -f \${container}\`))
|
|
255
|
+
await safe(() => docker.command(\`rmi -f \${image}\`))
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'help',
|
|
260
|
+
emoji: 'ℹ️',
|
|
261
|
+
description: 'Print commands',
|
|
262
|
+
handler: () => {
|
|
263
|
+
printHelp()
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
],
|
|
222
267
|
})
|
|
223
268
|
`;
|
|
224
269
|
}
|
package/dist/menu.js
CHANGED
|
@@ -4,7 +4,8 @@ import { releaseMultiple, releaseSingle } from './release.js';
|
|
|
4
4
|
import { getOrderedPackages } from './packages.js';
|
|
5
5
|
import { runHelperCli } from './helper-cli.js';
|
|
6
6
|
import { ensureWorkingTreeCommitted } from './preflight.js';
|
|
7
|
-
|
|
7
|
+
import { run } from './utils/run.js';
|
|
8
|
+
function makeManagerStepEntries(targets, packages, state) {
|
|
8
9
|
return [
|
|
9
10
|
{
|
|
10
11
|
name: 'update dependencies',
|
|
@@ -84,12 +85,29 @@ export function makeStepEntries(targets, packages, state) {
|
|
|
84
85
|
},
|
|
85
86
|
];
|
|
86
87
|
}
|
|
88
|
+
function makePackageScriptEntries(pkg) {
|
|
89
|
+
const scripts = pkg.json?.scripts ?? {};
|
|
90
|
+
const entries = [];
|
|
91
|
+
Object.entries(scripts)
|
|
92
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
93
|
+
.forEach(([name, command]) => {
|
|
94
|
+
entries.push({
|
|
95
|
+
name,
|
|
96
|
+
emoji: '▶️',
|
|
97
|
+
description: typeof command === 'string' ? command : '',
|
|
98
|
+
handler: async () => {
|
|
99
|
+
await run('pnpm', ['run', name], { cwd: pkg.path });
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
return entries;
|
|
104
|
+
}
|
|
87
105
|
export function buildPackageSelectionMenu(packages, onStepComplete) {
|
|
88
106
|
const ordered = getOrderedPackages(packages);
|
|
89
107
|
const entries = ordered.map((pkg) => ({
|
|
90
|
-
name: pkg.substitute,
|
|
108
|
+
name: pkg.name ?? pkg.substitute ?? pkg.dirName,
|
|
91
109
|
emoji: colors[pkg.color]('●'),
|
|
92
|
-
description: pkg.
|
|
110
|
+
description: pkg.relativeDir ?? pkg.dirName,
|
|
93
111
|
handler: async () => {
|
|
94
112
|
const step = await runStepLoop([pkg], packages);
|
|
95
113
|
onStepComplete?.(step);
|
|
@@ -112,6 +130,46 @@ export function buildPackageSelectionMenu(packages, onStepComplete) {
|
|
|
112
130
|
// This removes the infinite loop and returns to the step menu after each run.
|
|
113
131
|
export async function runStepLoop(targets, packages) {
|
|
114
132
|
const state = {};
|
|
133
|
+
// Single package: show combined menu (manager actions + package.json scripts)
|
|
134
|
+
if (targets.length === 1) {
|
|
135
|
+
const pkg = targets[0];
|
|
136
|
+
// eslint-disable-next-line no-constant-condition
|
|
137
|
+
while (true) {
|
|
138
|
+
const scriptEntries = makePackageScriptEntries(pkg);
|
|
139
|
+
const entries = [
|
|
140
|
+
{
|
|
141
|
+
name: 'manager actions',
|
|
142
|
+
emoji: globalEmoji,
|
|
143
|
+
description: 'update/test/build/publish',
|
|
144
|
+
handler: async () => {
|
|
145
|
+
const managerEntries = makeManagerStepEntries(targets, packages, state);
|
|
146
|
+
await runHelperCli({
|
|
147
|
+
title: `Manager actions for ${pkg.name}`,
|
|
148
|
+
scripts: managerEntries,
|
|
149
|
+
argv: [],
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
...scriptEntries,
|
|
154
|
+
{
|
|
155
|
+
name: 'back',
|
|
156
|
+
emoji: '↩️',
|
|
157
|
+
description: 'Pick packages again',
|
|
158
|
+
handler: () => {
|
|
159
|
+
state.lastStep = 'back';
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
await runHelperCli({
|
|
164
|
+
title: `Actions for ${pkg.name}`,
|
|
165
|
+
scripts: entries,
|
|
166
|
+
argv: [],
|
|
167
|
+
});
|
|
168
|
+
if (state.lastStep === 'back')
|
|
169
|
+
return state.lastStep;
|
|
170
|
+
// loop to keep showing menu
|
|
171
|
+
}
|
|
172
|
+
}
|
|
115
173
|
// Loop shows the step menu and executes a chosen action once.
|
|
116
174
|
// After the handler completes, show the menu again.
|
|
117
175
|
// Selecting "back" breaks and returns control to the package picker.
|
|
@@ -119,7 +177,7 @@ export async function runStepLoop(targets, packages) {
|
|
|
119
177
|
// eslint-disable-next-line no-constant-condition
|
|
120
178
|
while (true) {
|
|
121
179
|
state.lastStep = undefined;
|
|
122
|
-
const entries =
|
|
180
|
+
const entries = makeManagerStepEntries(targets, packages, state);
|
|
123
181
|
await runHelperCli({
|
|
124
182
|
title: `Actions for ${targets.length === 1 ? targets[0].name : 'selected packages'}`,
|
|
125
183
|
scripts: entries,
|
package/dist/prompts.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// src/prompts.js
|
|
2
2
|
import readline from 'node:readline/promises';
|
|
3
3
|
import { stdin as input, stdout as output } from 'node:process';
|
|
4
|
-
export const publishCliState = {
|
|
4
|
+
export const publishCliState = {
|
|
5
|
+
autoDecision: undefined,
|
|
6
|
+
};
|
|
5
7
|
export function promptSingleKey(message, resolver) {
|
|
6
8
|
const supportsRawMode = typeof input.setRawMode === 'function' && input.isTTY;
|
|
7
9
|
if (!supportsRawMode) {
|
|
@@ -64,22 +66,26 @@ export async function askLine(question) {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
export async function promptYesNoAll(question) {
|
|
67
|
-
if (publishCliState.
|
|
68
|
-
console.log(`${question} (auto
|
|
69
|
-
return
|
|
69
|
+
if (publishCliState.autoDecision) {
|
|
70
|
+
console.log(`${question} (auto-${publishCliState.autoDecision} via "all")`);
|
|
71
|
+
return publishCliState.autoDecision;
|
|
70
72
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
// Allow yes/no + "apply to all" variants: y, ya, n, na
|
|
74
|
+
// Use readline-style input to capture multi-character tokens consistently.
|
|
75
|
+
// eslint-disable-next-line no-constant-condition
|
|
76
|
+
while (true) {
|
|
77
|
+
const answer = (await askLine(`${question} (y/ya/n/na): `)).toLowerCase();
|
|
78
|
+
if (answer === 'y')
|
|
73
79
|
return 'yes';
|
|
74
|
-
if (
|
|
80
|
+
if (answer === 'ya') {
|
|
81
|
+
publishCliState.autoDecision = 'yes';
|
|
82
|
+
return 'yes';
|
|
83
|
+
}
|
|
84
|
+
if (answer === 'n')
|
|
75
85
|
return 'no';
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (result === 'all') {
|
|
81
|
-
publishCliState.autoConfirmAll = true;
|
|
82
|
-
return 'yes';
|
|
86
|
+
if (answer === 'na') {
|
|
87
|
+
publishCliState.autoDecision = 'no';
|
|
88
|
+
return 'no';
|
|
89
|
+
}
|
|
83
90
|
}
|
|
84
|
-
return result;
|
|
85
91
|
}
|
package/dist/publish.js
CHANGED
|
@@ -136,7 +136,7 @@ async function main() {
|
|
|
136
136
|
if (packages.length === 0) {
|
|
137
137
|
throw new Error('No packages with a package.json found in this workspace');
|
|
138
138
|
}
|
|
139
|
-
publishCliState.
|
|
139
|
+
publishCliState.autoDecision = 'yes';
|
|
140
140
|
if (!parsed.selectionArg) {
|
|
141
141
|
throw new Error('Non-interactive mode requires a package selection: <pkg> or "all".');
|
|
142
142
|
}
|