@aurorajs.dev/catalyst-cli 1.0.0 → 1.0.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/README.md +25 -25
- package/dist/commands/generate/index.d.ts +3 -0
- package/dist/commands/generate/index.js +41 -4
- package/dist/commands/origin/review.js +7 -87
- package/dist/origin/interactive/review-mode.d.ts +25 -0
- package/dist/origin/interactive/review-mode.js +22 -0
- package/dist/origin/interactive/review-session.d.ts +36 -0
- package/dist/origin/interactive/review-session.js +123 -0
- package/dist/templates/backend/env/.env +1 -1
- package/dist/templates/codegen/partials/relationships/async-multiple-search-select.eta +8 -3
- package/dist/templates/codegen/partials/relationships/async-search-select.eta +8 -3
- package/dist/templates/codegen/partials/relationships/multiple-search-select.eta +8 -3
- package/dist/templates/codegen/partials/relationships/search-select.eta +8 -3
- package/oclif.manifest.json +52 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# aurora-
|
|
1
|
+
# aurora-catalyst-cli
|
|
2
2
|
|
|
3
3
|
A new CLI generated with oclif
|
|
4
4
|
|
|
5
5
|
[](https://oclif.io)
|
|
6
|
-
[](https://npmjs.org/package/aurora-catalyst-cli)
|
|
7
|
+
[](https://npmjs.org/package/aurora-catalyst-cli)
|
|
8
8
|
|
|
9
9
|
<!-- toc -->
|
|
10
|
-
* [aurora-
|
|
10
|
+
* [aurora-catalyst-cli](#aurora-catalyst-cli)
|
|
11
11
|
* [Usage](#usage)
|
|
12
12
|
* [Commands](#commands)
|
|
13
13
|
* [Command Topics](#command-topics)
|
|
@@ -48,7 +48,7 @@ $ npm install -g @aurorajs.dev/catalyst-cli
|
|
|
48
48
|
$ catalyst COMMAND
|
|
49
49
|
running command...
|
|
50
50
|
$ catalyst (--version)
|
|
51
|
-
@aurorajs.dev/catalyst-cli/1.0.
|
|
51
|
+
@aurorajs.dev/catalyst-cli/1.0.2 darwin-arm64 node-v24.14.0
|
|
52
52
|
$ catalyst --help [COMMAND]
|
|
53
53
|
USAGE
|
|
54
54
|
$ catalyst COMMAND
|
|
@@ -121,24 +121,24 @@ YAML schemas are expected in `./cliter/{bounded_context}/*.aurora.yaml`.
|
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
123
|
# List configured bounded contexts
|
|
124
|
-
|
|
124
|
+
catalyst sheets list
|
|
125
125
|
|
|
126
126
|
# Validate connection to a spreadsheet
|
|
127
|
-
|
|
127
|
+
catalyst sheets validate --bc iam
|
|
128
128
|
|
|
129
129
|
# Push YAML schemas to Google Sheets
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
catalyst sheets push --bc iam
|
|
131
|
+
catalyst sheets push --all
|
|
132
|
+
catalyst sheets push --all --dry-run
|
|
133
133
|
|
|
134
134
|
# Pull from Google Sheets to YAML files
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
catalyst sheets pull --bc iam
|
|
136
|
+
catalyst sheets pull --all
|
|
137
|
+
catalyst sheets pull --all --no-backup
|
|
138
138
|
|
|
139
139
|
# Show differences between YAML and Sheets
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
catalyst sheets diff --bc iam
|
|
141
|
+
catalyst sheets diff --all --summary
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
Without `--bc` or `--all`, commands prompt interactively for the bounded context.
|
|
@@ -161,16 +161,16 @@ environments:
|
|
|
161
161
|
|
|
162
162
|
```bash
|
|
163
163
|
# Deploy all environments
|
|
164
|
-
|
|
164
|
+
catalyst deploy
|
|
165
165
|
|
|
166
166
|
# Deploy only dev
|
|
167
|
-
|
|
167
|
+
catalyst deploy --env dev
|
|
168
168
|
|
|
169
169
|
# Dry run — show what would be pushed without executing
|
|
170
|
-
|
|
170
|
+
catalyst deploy --dry-run
|
|
171
171
|
|
|
172
172
|
# Custom config path
|
|
173
|
-
|
|
173
|
+
catalyst deploy --env prod -c ../aurora.yaml
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
Internally, the command runs `git push origin {source}:environments/{env}` for each resolved environment.
|
|
@@ -234,22 +234,22 @@ Copy `aurora.example.yaml` to `aurora.yaml` in your project and fill in your val
|
|
|
234
234
|
|
|
235
235
|
```bash
|
|
236
236
|
# Provision all environments (passwords from .env)
|
|
237
|
-
|
|
237
|
+
catalyst provision
|
|
238
238
|
|
|
239
239
|
# Provision only dev
|
|
240
|
-
|
|
240
|
+
catalyst provision --env dev
|
|
241
241
|
|
|
242
242
|
# Provision only prod backend
|
|
243
|
-
|
|
243
|
+
catalyst provision --env prod --target back
|
|
244
244
|
|
|
245
245
|
# Provision only frontend (all environments)
|
|
246
|
-
|
|
246
|
+
catalyst provision --target front
|
|
247
247
|
|
|
248
248
|
# Skip DNS and CI/CD (applies to all plans)
|
|
249
|
-
|
|
249
|
+
catalyst provision --skip-dns --skip-cicd
|
|
250
250
|
|
|
251
251
|
# Pass passwords inline (overrides .env)
|
|
252
|
-
|
|
252
|
+
catalyst provision --ftp-password s3cret --db-password dbpass
|
|
253
253
|
```
|
|
254
254
|
|
|
255
255
|
GitHub secrets and variables are prefixed with `ENV_TARGET_` (e.g., `DEV_BACK_SSH_PRIVATE_KEY`, `PROD_FRONT_SERVER_HOST`).
|
|
@@ -9,12 +9,15 @@ export default class Generate extends Command {
|
|
|
9
9
|
static flags: {
|
|
10
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
name: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'no-review': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
noGraphQLTypes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
overwriteInterface: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
review: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
16
|
target: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
17
|
tests: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
16
18
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
19
|
};
|
|
18
20
|
run(): Promise<void>;
|
|
21
|
+
private logOriginSummary;
|
|
19
22
|
private parseName;
|
|
20
23
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
3
4
|
import { loadSchema } from '../../generator/domain/schema-loader.js';
|
|
4
5
|
import { loadLockFiles } from '../../generator/engine/lock-file.js';
|
|
5
6
|
import { generateBackModule } from '../../generator/handlers/back.handler.js';
|
|
6
7
|
import { generateFrontModule } from '../../generator/handlers/front.handler.js';
|
|
8
|
+
import { toCurrentPath } from '../../origin/domain/origin-file.js';
|
|
9
|
+
import { resolveReviewMode } from '../../origin/interactive/review-mode.js';
|
|
10
|
+
import { createDefaultReviewSessionIo, runReviewSession, } from '../../origin/interactive/review-session.js';
|
|
7
11
|
export default class Generate extends Command {
|
|
8
12
|
static args = {
|
|
9
13
|
scope: Args.string({
|
|
@@ -36,6 +40,11 @@ export default class Generate extends Command {
|
|
|
36
40
|
description: 'Bounded context and module (e.g. "iam/account").',
|
|
37
41
|
required: true,
|
|
38
42
|
}),
|
|
43
|
+
'no-review': Flags.boolean({
|
|
44
|
+
default: false,
|
|
45
|
+
description: 'Skip the interactive review of generated `.origin` files.',
|
|
46
|
+
exclusive: ['review'],
|
|
47
|
+
}),
|
|
39
48
|
noGraphQLTypes: Flags.boolean({
|
|
40
49
|
char: 'g',
|
|
41
50
|
default: false,
|
|
@@ -46,6 +55,11 @@ export default class Generate extends Command {
|
|
|
46
55
|
default: false,
|
|
47
56
|
description: 'Overwrite front-end interfaces.',
|
|
48
57
|
}),
|
|
58
|
+
review: Flags.boolean({
|
|
59
|
+
default: false,
|
|
60
|
+
description: 'Force the interactive review of generated `.origin` files.',
|
|
61
|
+
exclusive: ['no-review'],
|
|
62
|
+
}),
|
|
49
63
|
target: Flags.string({
|
|
50
64
|
description: 'Output subdirectory (default: "backend" for back, "frontend" for front).',
|
|
51
65
|
}),
|
|
@@ -106,11 +120,34 @@ export default class Generate extends Command {
|
|
|
106
120
|
originFiles = result.originFiles;
|
|
107
121
|
this.log(`Front-end module "${boundedContextName}/${moduleName}" generated.`);
|
|
108
122
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
// Nothing produced → no review, no hint, regardless of mode or TTY.
|
|
124
|
+
if (originFiles.length === 0)
|
|
125
|
+
return;
|
|
126
|
+
const mode = resolveReviewMode({
|
|
127
|
+
isTty: Boolean(process.stdout.isTTY),
|
|
128
|
+
noReview: flags['no-review'],
|
|
129
|
+
review: flags.review,
|
|
130
|
+
});
|
|
131
|
+
if (mode === 'skip') {
|
|
132
|
+
this.logOriginSummary(originFiles.length);
|
|
133
|
+
return;
|
|
113
134
|
}
|
|
135
|
+
// Interactive: walk the user through the files this run produced, then
|
|
136
|
+
// report any they left pending (e.g. they chose Finish early).
|
|
137
|
+
const entries = originFiles.map((origin) => ({
|
|
138
|
+
current: toCurrentPath(origin),
|
|
139
|
+
origin,
|
|
140
|
+
}));
|
|
141
|
+
await runReviewSession(entries, createDefaultReviewSessionIo((message) => {
|
|
142
|
+
this.log(message);
|
|
143
|
+
}));
|
|
144
|
+
const residual = originFiles.filter((origin) => fs.existsSync(origin));
|
|
145
|
+
if (residual.length > 0)
|
|
146
|
+
this.logOriginSummary(residual.length);
|
|
147
|
+
}
|
|
148
|
+
logOriginSummary(count) {
|
|
149
|
+
const noun = count === 1 ? 'file' : 'files';
|
|
150
|
+
this.log(`${count} origin ${noun} pending review. Run "catalyst origin list" to inspect or "catalyst origin review" for an interactive walk-through.`);
|
|
114
151
|
}
|
|
115
152
|
parseName(name) {
|
|
116
153
|
const parts = name.split('/');
|
|
@@ -1,97 +1,17 @@
|
|
|
1
|
-
import select from '@inquirer/select';
|
|
2
1
|
import { Command } from '@oclif/core';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { acceptOrigin, ignoreOrigin, rejectOrigin, } from '../../origin/domain/actions.js';
|
|
7
|
-
import { discoverOrigins, } from '../../origin/domain/origin-file.js';
|
|
2
|
+
import { discoverOrigins } from '../../origin/domain/origin-file.js';
|
|
3
|
+
import { createDefaultReviewSessionIo, runReviewSession, } from '../../origin/interactive/review-session.js';
|
|
8
4
|
export default class OriginReview extends Command {
|
|
9
5
|
static description = 'Interactive walk-through of pending `.origin` files (inquirer + VS Code diff).';
|
|
10
6
|
static examples = ['<%= config.bin %> origin review'];
|
|
11
7
|
async run() {
|
|
12
|
-
|
|
13
|
-
if (
|
|
8
|
+
const entries = discoverOrigins();
|
|
9
|
+
if (entries.length === 0) {
|
|
14
10
|
this.log('No origin files to review.');
|
|
15
11
|
return;
|
|
16
12
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!selected)
|
|
21
|
-
return;
|
|
22
|
-
openDiffInVsCode(selected);
|
|
23
|
-
// eslint-disable-next-line no-await-in-loop
|
|
24
|
-
const action = await selectAction();
|
|
25
|
-
switch (action) {
|
|
26
|
-
case 'accept': {
|
|
27
|
-
const result = acceptOrigin(selected.current);
|
|
28
|
-
this.log(`${chalk.green('[ACCEPTED]')} ${result.path}${result.status === 'noop' ? chalk.dim(' (noop)') : ''}`);
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
case 'finish': {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
case 'go-back': {
|
|
35
|
-
// Loop again to refresh the file picker.
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
case 'ignore': {
|
|
39
|
-
const result = ignoreOrigin(selected.current);
|
|
40
|
-
this.log(`${chalk.cyan('[IGNORED]')} ${result.path}${result.status === 'noop' ? chalk.dim(' (noop)') : ''}`);
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
case 'reject': {
|
|
44
|
-
const result = rejectOrigin(selected.current);
|
|
45
|
-
this.log(`${chalk.yellow('[REJECTED]')} ${result.path}${result.status === 'noop' ? chalk.dim(' (noop)') : ''}`);
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// Refresh the list — the action just removed an entry (or kept it on
|
|
50
|
-
// 'go-back'). Re-scanning is cheap and avoids manual list bookkeeping.
|
|
51
|
-
remaining = discoverOrigins();
|
|
52
|
-
}
|
|
53
|
-
this.log('All origin files processed.');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async function selectFile(entries) {
|
|
57
|
-
if (entries.length === 0)
|
|
58
|
-
return undefined;
|
|
59
|
-
return select({
|
|
60
|
-
choices: entries.map((entry) => ({ name: entry.current, value: entry })),
|
|
61
|
-
message: 'Select file to review:',
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
async function selectAction() {
|
|
65
|
-
return select({
|
|
66
|
-
choices: [
|
|
67
|
-
{ name: 'Reject (keep my current file as-is)', value: 'reject' },
|
|
68
|
-
{ name: 'Accept (adopt the codegen proposal)', value: 'accept' },
|
|
69
|
-
{
|
|
70
|
-
name: 'Ignore (keep mine + mark file as ignored on future regens)',
|
|
71
|
-
value: 'ignore',
|
|
72
|
-
},
|
|
73
|
-
{ name: 'Go back to file selection', value: 'go-back' },
|
|
74
|
-
{ name: 'Finish review', value: 'finish' },
|
|
75
|
-
],
|
|
76
|
-
message: 'Select an action:',
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
function openDiffInVsCode(entry) {
|
|
80
|
-
const absCurrent = path.resolve(entry.current);
|
|
81
|
-
const absOrigin = path.resolve(entry.origin);
|
|
82
|
-
try {
|
|
83
|
-
// Order matters: `code --diff <left> <right>` puts the first path on the
|
|
84
|
-
// left (source of changes) and the second on the right (editable
|
|
85
|
-
// destination). Reviewing means importing pieces of the codegen proposal
|
|
86
|
-
// (`.origin`) into the user's file, so `.origin` goes left and the
|
|
87
|
-
// current file goes right — that way the inline arrows move chunks from
|
|
88
|
-
// proposal → current file, not the other way around.
|
|
89
|
-
execFileSync('code', ['--diff', absOrigin, absCurrent], {
|
|
90
|
-
stdio: 'ignore',
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// VS Code not on PATH — silently continue. The user can still pick an
|
|
95
|
-
// action without the side-by-side diff.
|
|
13
|
+
await runReviewSession(entries, createDefaultReviewSessionIo((message) => {
|
|
14
|
+
this.log(message);
|
|
15
|
+
}));
|
|
96
16
|
}
|
|
97
17
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resolution of `catalyst generate`'s end-of-run review mode. Kept free
|
|
3
|
+
* of I/O so the precedence (flags over TTY) is unit-testable without spawning
|
|
4
|
+
* a process or a prompt.
|
|
5
|
+
*/
|
|
6
|
+
export type ReviewMode = 'interactive' | 'skip';
|
|
7
|
+
export interface ReviewModeInputs {
|
|
8
|
+
/** `Boolean(process.stdout.isTTY)` at the call site. */
|
|
9
|
+
isTty: boolean;
|
|
10
|
+
/** The `--no-review` flag. */
|
|
11
|
+
noReview: boolean;
|
|
12
|
+
/** The `--review` flag. */
|
|
13
|
+
review: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Precedence: an explicit flag always wins over TTY detection.
|
|
17
|
+
* - `--review` → interactive
|
|
18
|
+
* - `--no-review` → skip
|
|
19
|
+
* - neither → interactive on a TTY, skip otherwise
|
|
20
|
+
*
|
|
21
|
+
* `--review` and `--no-review` are declared mutually exclusive at the oclif
|
|
22
|
+
* flag layer, so both being true never reaches here; if it ever did,
|
|
23
|
+
* `--review` wins.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveReviewMode({ isTty, noReview, review, }: ReviewModeInputs): ReviewMode;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure resolution of `catalyst generate`'s end-of-run review mode. Kept free
|
|
3
|
+
* of I/O so the precedence (flags over TTY) is unit-testable without spawning
|
|
4
|
+
* a process or a prompt.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Precedence: an explicit flag always wins over TTY detection.
|
|
8
|
+
* - `--review` → interactive
|
|
9
|
+
* - `--no-review` → skip
|
|
10
|
+
* - neither → interactive on a TTY, skip otherwise
|
|
11
|
+
*
|
|
12
|
+
* `--review` and `--no-review` are declared mutually exclusive at the oclif
|
|
13
|
+
* flag layer, so both being true never reaches here; if it ever did,
|
|
14
|
+
* `--review` wins.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveReviewMode({ isTty, noReview, review, }) {
|
|
17
|
+
if (review)
|
|
18
|
+
return 'interactive';
|
|
19
|
+
if (noReview)
|
|
20
|
+
return 'skip';
|
|
21
|
+
return isTty ? 'interactive' : 'skip';
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared interactive `.origin` walk-through. This is the single
|
|
3
|
+
* implementation of the review loop; both `catalyst origin review` and
|
|
4
|
+
* `catalyst generate`'s interactive mode delegate to `runReviewSession`.
|
|
5
|
+
*
|
|
6
|
+
* Action logic lives in the domain (`acceptOrigin`/`rejectOrigin`/
|
|
7
|
+
* `ignoreOrigin`). This module owns only the orchestration plus the I/O
|
|
8
|
+
* concerns (prompts, the `code --diff` viewer, log formatting), which are
|
|
9
|
+
* injectable via `ReviewSessionIo` so the loop is testable without driving
|
|
10
|
+
* real prompts. These I/O concerns MUST NOT bleed into the domain.
|
|
11
|
+
*/
|
|
12
|
+
import { type OriginEntry } from '../domain/origin-file.js';
|
|
13
|
+
export type ReviewAction = 'accept' | 'finish' | 'go-back' | 'ignore' | 'reject';
|
|
14
|
+
/**
|
|
15
|
+
* The injectable I/O seam. The default implementation uses `@inquirer/select`
|
|
16
|
+
* for prompts and `code --diff` for the diff view; tests inject a stub.
|
|
17
|
+
*/
|
|
18
|
+
export interface ReviewSessionIo {
|
|
19
|
+
log(message: string): void;
|
|
20
|
+
openDiff(entry: OriginEntry): void;
|
|
21
|
+
selectAction(): Promise<ReviewAction>;
|
|
22
|
+
selectFile(entries: OriginEntry[]): Promise<OriginEntry | undefined>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Walk the user through the given `.origin` entries. The entry set is scoped
|
|
26
|
+
* by the caller: `origin review` passes the full cwd discovery, `generate`
|
|
27
|
+
* passes only the files its run produced. "Remaining" is refreshed by
|
|
28
|
+
* checking which `.origin` files still exist on disk after each action — an
|
|
29
|
+
* entry is done when its `.origin` is gone — so the loop never re-scans cwd.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runReviewSession(entries: OriginEntry[], io?: ReviewSessionIo): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Default I/O: inquirer prompts + VS Code diff. `log` defaults to stdout but
|
|
34
|
+
* callers (oclif commands) inject `this.log` to route through oclif output.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createDefaultReviewSessionIo(log?: (message: string) => void): ReviewSessionIo;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared interactive `.origin` walk-through. This is the single
|
|
3
|
+
* implementation of the review loop; both `catalyst origin review` and
|
|
4
|
+
* `catalyst generate`'s interactive mode delegate to `runReviewSession`.
|
|
5
|
+
*
|
|
6
|
+
* Action logic lives in the domain (`acceptOrigin`/`rejectOrigin`/
|
|
7
|
+
* `ignoreOrigin`). This module owns only the orchestration plus the I/O
|
|
8
|
+
* concerns (prompts, the `code --diff` viewer, log formatting), which are
|
|
9
|
+
* injectable via `ReviewSessionIo` so the loop is testable without driving
|
|
10
|
+
* real prompts. These I/O concerns MUST NOT bleed into the domain.
|
|
11
|
+
*/
|
|
12
|
+
import select from '@inquirer/select';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { acceptOrigin, ignoreOrigin, rejectOrigin, } from '../domain/actions.js';
|
|
18
|
+
/**
|
|
19
|
+
* Walk the user through the given `.origin` entries. The entry set is scoped
|
|
20
|
+
* by the caller: `origin review` passes the full cwd discovery, `generate`
|
|
21
|
+
* passes only the files its run produced. "Remaining" is refreshed by
|
|
22
|
+
* checking which `.origin` files still exist on disk after each action — an
|
|
23
|
+
* entry is done when its `.origin` is gone — so the loop never re-scans cwd.
|
|
24
|
+
*/
|
|
25
|
+
export async function runReviewSession(entries, io = createDefaultReviewSessionIo()) {
|
|
26
|
+
let remaining = entries.filter((entry) => stillPending(entry));
|
|
27
|
+
if (remaining.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
while (remaining.length > 0) {
|
|
30
|
+
// eslint-disable-next-line no-await-in-loop
|
|
31
|
+
const selected = await io.selectFile(remaining);
|
|
32
|
+
if (!selected)
|
|
33
|
+
return;
|
|
34
|
+
io.openDiff(selected);
|
|
35
|
+
// eslint-disable-next-line no-await-in-loop
|
|
36
|
+
const action = await io.selectAction();
|
|
37
|
+
if (action === 'finish')
|
|
38
|
+
return;
|
|
39
|
+
if (action !== 'go-back') {
|
|
40
|
+
io.log(applyAction(action, selected));
|
|
41
|
+
}
|
|
42
|
+
// Re-derive from the scoped set: an entry is done once its `.origin` is
|
|
43
|
+
// gone (accept renames it away; reject/ignore delete it). 'go-back' keeps
|
|
44
|
+
// the entry, so the list is unchanged on that branch.
|
|
45
|
+
remaining = remaining.filter((entry) => stillPending(entry));
|
|
46
|
+
}
|
|
47
|
+
io.log('All origin files processed.');
|
|
48
|
+
}
|
|
49
|
+
function applyAction(action, entry) {
|
|
50
|
+
switch (action) {
|
|
51
|
+
case 'accept': {
|
|
52
|
+
const result = acceptOrigin(entry.current);
|
|
53
|
+
return `${chalk.green('[ACCEPTED]')} ${result.path}${result.status === 'noop' ? chalk.dim(' (noop)') : ''}`;
|
|
54
|
+
}
|
|
55
|
+
case 'ignore': {
|
|
56
|
+
const result = ignoreOrigin(entry.current);
|
|
57
|
+
return `${chalk.cyan('[IGNORED]')} ${result.path}${result.status === 'noop' ? chalk.dim(' (noop)') : ''}`;
|
|
58
|
+
}
|
|
59
|
+
case 'reject': {
|
|
60
|
+
const result = rejectOrigin(entry.current);
|
|
61
|
+
return `${chalk.yellow('[REJECTED]')} ${result.path}${result.status === 'noop' ? chalk.dim(' (noop)') : ''}`;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function stillPending(entry) {
|
|
66
|
+
return fs.existsSync(entry.origin);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Default I/O: inquirer prompts + VS Code diff. `log` defaults to stdout but
|
|
70
|
+
* callers (oclif commands) inject `this.log` to route through oclif output.
|
|
71
|
+
*/
|
|
72
|
+
export function createDefaultReviewSessionIo(log = (message) => {
|
|
73
|
+
console.log(message);
|
|
74
|
+
}) {
|
|
75
|
+
return {
|
|
76
|
+
log,
|
|
77
|
+
openDiff: openDiffInVsCode,
|
|
78
|
+
selectAction,
|
|
79
|
+
selectFile,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function selectFile(entries) {
|
|
83
|
+
if (entries.length === 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
return select({
|
|
86
|
+
choices: entries.map((entry) => ({ name: entry.current, value: entry })),
|
|
87
|
+
message: 'Select file to review:',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async function selectAction() {
|
|
91
|
+
return select({
|
|
92
|
+
choices: [
|
|
93
|
+
{ name: 'Reject (keep my current file as-is)', value: 'reject' },
|
|
94
|
+
{ name: 'Accept (adopt the codegen proposal)', value: 'accept' },
|
|
95
|
+
{
|
|
96
|
+
name: 'Ignore (keep mine + mark file as ignored on future regens)',
|
|
97
|
+
value: 'ignore',
|
|
98
|
+
},
|
|
99
|
+
{ name: 'Go back to file selection', value: 'go-back' },
|
|
100
|
+
{ name: 'Finish review', value: 'finish' },
|
|
101
|
+
],
|
|
102
|
+
message: 'Select an action:',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function openDiffInVsCode(entry) {
|
|
106
|
+
const absCurrent = path.resolve(entry.current);
|
|
107
|
+
const absOrigin = path.resolve(entry.origin);
|
|
108
|
+
try {
|
|
109
|
+
// Order matters: `code --diff <left> <right>` puts the first path on the
|
|
110
|
+
// left (source of changes) and the second on the right (editable
|
|
111
|
+
// destination). Reviewing means importing pieces of the codegen proposal
|
|
112
|
+
// (`.origin`) into the user's file, so `.origin` goes left and the
|
|
113
|
+
// current file goes right — that way the inline arrows move chunks from
|
|
114
|
+
// proposal → current file, not the other way around.
|
|
115
|
+
execFileSync('code', ['--diff', absOrigin, absCurrent], {
|
|
116
|
+
stdio: 'ignore',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// VS Code not on PATH — silently continue. The user can still pick an
|
|
121
|
+
// action without the side-by-side diff.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
// [inputId] binding ties <label for=...> to the internal Spartan
|
|
14
14
|
// <hlm-combobox-input id=...>.
|
|
15
15
|
const relSingular = it.fmt.toCamelCase(it.mappers.getRelationshipSingularName(it.property));
|
|
16
|
-
const optionsName = relSingular + 'Options';
|
|
17
|
-
|
|
16
|
+
const optionsName = it.property.relationship?.modulePath ? relSingular + 'Options' : it.propCamel;
|
|
17
|
+
// A relational widget can land on a property WITHOUT a relationship (e.g. a
|
|
18
|
+
// scalar `array<varchar>`). Only resolve the target displayField when there
|
|
19
|
+
// is a relationship; otherwise render without the pipe (pre-regression shape).
|
|
20
|
+
const displayFieldTemplate = it.property.relationship?.modulePath
|
|
21
|
+
? it.crossSchema.getTargetDisplayFieldTemplate(it.property.relationship.modulePath)
|
|
22
|
+
: null;
|
|
18
23
|
const isRequired = it.property.nullable === false && it.property.type !== 'boolean';
|
|
19
24
|
_%>
|
|
20
25
|
<hlm-field class="<%= it.colSpan %>">
|
|
@@ -22,7 +27,7 @@ _%>
|
|
|
22
27
|
<au-async-multiple-search-select
|
|
23
28
|
[inputId]="'<%= it.propCamel %>'"
|
|
24
29
|
[value]="signalForm.controls['<%= it.propCamel %>'].value ?? []"
|
|
25
|
-
[options]="<%= optionsName %>() | toOptions:'<%= displayFieldTemplate %>'"
|
|
30
|
+
[options]="<%= optionsName %>()<% if (displayFieldTemplate) { %> | toOptions:'<%= displayFieldTemplate %>'<% } %>"
|
|
26
31
|
[loading]="<%= relSingular %>Loading()"
|
|
27
32
|
[placeholder]="t('Select')"
|
|
28
33
|
[emptyMessage]="t('NoResults')"
|
|
@@ -14,8 +14,13 @@
|
|
|
14
14
|
// [inputId] binding ties <label for=...> to the internal Spartan
|
|
15
15
|
// <hlm-combobox-input id=...>.
|
|
16
16
|
const relSingular = it.fmt.toCamelCase(it.mappers.getRelationshipSingularName(it.property));
|
|
17
|
-
const optionsName = relSingular + 'Options';
|
|
18
|
-
|
|
17
|
+
const optionsName = it.property.relationship?.modulePath ? relSingular + 'Options' : it.propCamel;
|
|
18
|
+
// A relational widget can land on a property WITHOUT a relationship (e.g. a
|
|
19
|
+
// scalar `array<varchar>`). Only resolve the target displayField when there
|
|
20
|
+
// is a relationship; otherwise render without the pipe (pre-regression shape).
|
|
21
|
+
const displayFieldTemplate = it.property.relationship?.modulePath
|
|
22
|
+
? it.crossSchema.getTargetDisplayFieldTemplate(it.property.relationship.modulePath)
|
|
23
|
+
: null;
|
|
19
24
|
const isRequired = it.property.nullable === false && it.property.type !== 'boolean';
|
|
20
25
|
_%>
|
|
21
26
|
<hlm-field class="<%= it.colSpan %>">
|
|
@@ -23,7 +28,7 @@ _%>
|
|
|
23
28
|
<au-async-search-select
|
|
24
29
|
[inputId]="'<%= it.propCamel %>'"
|
|
25
30
|
[value]="signalForm.controls['<%= it.propCamel %>'].value"
|
|
26
|
-
[options]="<%= optionsName %>() | toOptions:'<%= displayFieldTemplate %>'"
|
|
31
|
+
[options]="<%= optionsName %>()<% if (displayFieldTemplate) { %> | toOptions:'<%= displayFieldTemplate %>'<% } %>"
|
|
27
32
|
[loading]="<%= relSingular %>Loading()"
|
|
28
33
|
[placeholder]="t('Select')"
|
|
29
34
|
[emptyMessage]="t('NoResults')"
|
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
// [inputId] binding ties <label for=...> to the internal Spartan
|
|
12
12
|
// <hlm-combobox-input id=...>.
|
|
13
13
|
const relSingular = it.fmt.toCamelCase(it.mappers.getRelationshipSingularName(it.property));
|
|
14
|
-
const optionsName = relSingular + 'Options';
|
|
15
|
-
|
|
14
|
+
const optionsName = it.property.relationship?.modulePath ? relSingular + 'Options' : it.propCamel;
|
|
15
|
+
// A relational widget can land on a property WITHOUT a relationship (e.g. a
|
|
16
|
+
// scalar `array<varchar>`). Only resolve the target displayField when there
|
|
17
|
+
// is a relationship; otherwise render without the pipe (pre-regression shape).
|
|
18
|
+
const displayFieldTemplate = it.property.relationship?.modulePath
|
|
19
|
+
? it.crossSchema.getTargetDisplayFieldTemplate(it.property.relationship.modulePath)
|
|
20
|
+
: null;
|
|
16
21
|
const isRequired = it.property.nullable === false && it.property.type !== 'boolean';
|
|
17
22
|
_%>
|
|
18
23
|
<hlm-field class="<%= it.colSpan %>">
|
|
@@ -20,7 +25,7 @@ _%>
|
|
|
20
25
|
<au-multiple-search-select
|
|
21
26
|
[inputId]="'<%= it.propCamel %>'"
|
|
22
27
|
[value]="signalForm.controls['<%= it.propCamel %>'].value ?? []"
|
|
23
|
-
[options]="<%= optionsName %>() | toOptions:'<%= displayFieldTemplate %>'"
|
|
28
|
+
[options]="<%= optionsName %>()<% if (displayFieldTemplate) { %> | toOptions:'<%= displayFieldTemplate %>'<% } %>"
|
|
24
29
|
[placeholder]="t('Select')"
|
|
25
30
|
[emptyMessage]="t('NoResults')"
|
|
26
31
|
<% if (isRequired) { -%>
|
|
@@ -17,8 +17,13 @@
|
|
|
17
17
|
// input (depends on the companion `aurora-catalyst` PR exposing `inputId`
|
|
18
18
|
// on the wrapper).
|
|
19
19
|
const relSingular = it.fmt.toCamelCase(it.mappers.getRelationshipSingularName(it.property));
|
|
20
|
-
const optionsName = relSingular + 'Options';
|
|
21
|
-
|
|
20
|
+
const optionsName = it.property.relationship?.modulePath ? relSingular + 'Options' : it.propCamel;
|
|
21
|
+
// A relational widget can land on a property WITHOUT a relationship (e.g. a
|
|
22
|
+
// scalar `array<varchar>`). Only resolve the target displayField when there
|
|
23
|
+
// is a relationship; otherwise render without the pipe (pre-regression shape).
|
|
24
|
+
const displayFieldTemplate = it.property.relationship?.modulePath
|
|
25
|
+
? it.crossSchema.getTargetDisplayFieldTemplate(it.property.relationship.modulePath)
|
|
26
|
+
: null;
|
|
22
27
|
const isRequired = it.property.nullable === false && it.property.type !== 'boolean';
|
|
23
28
|
_%>
|
|
24
29
|
<hlm-field class="<%= it.colSpan %>">
|
|
@@ -26,7 +31,7 @@ _%>
|
|
|
26
31
|
<au-search-select
|
|
27
32
|
[inputId]="'<%= it.propCamel %>'"
|
|
28
33
|
[value]="signalForm.controls['<%= it.propCamel %>'].value"
|
|
29
|
-
[options]="<%= optionsName %>() | toOptions:'<%= displayFieldTemplate %>'"
|
|
34
|
+
[options]="<%= optionsName %>()<% if (displayFieldTemplate) { %> | toOptions:'<%= displayFieldTemplate %>'<% } %>"
|
|
30
35
|
[placeholder]="t('Select')"
|
|
31
36
|
[emptyMessage]="t('NoResults')"
|
|
32
37
|
<% if (isRequired) { -%>
|
package/oclif.manifest.json
CHANGED
|
@@ -322,6 +322,15 @@
|
|
|
322
322
|
"multiple": false,
|
|
323
323
|
"type": "option"
|
|
324
324
|
},
|
|
325
|
+
"no-review": {
|
|
326
|
+
"description": "Skip the interactive review of generated `.origin` files.",
|
|
327
|
+
"exclusive": [
|
|
328
|
+
"review"
|
|
329
|
+
],
|
|
330
|
+
"name": "no-review",
|
|
331
|
+
"allowNo": false,
|
|
332
|
+
"type": "boolean"
|
|
333
|
+
},
|
|
325
334
|
"noGraphQLTypes": {
|
|
326
335
|
"char": "g",
|
|
327
336
|
"description": "Skip GraphQL type generation.",
|
|
@@ -336,6 +345,15 @@
|
|
|
336
345
|
"allowNo": false,
|
|
337
346
|
"type": "boolean"
|
|
338
347
|
},
|
|
348
|
+
"review": {
|
|
349
|
+
"description": "Force the interactive review of generated `.origin` files.",
|
|
350
|
+
"exclusive": [
|
|
351
|
+
"no-review"
|
|
352
|
+
],
|
|
353
|
+
"name": "review",
|
|
354
|
+
"allowNo": false,
|
|
355
|
+
"type": "boolean"
|
|
356
|
+
},
|
|
339
357
|
"target": {
|
|
340
358
|
"description": "Output subdirectory (default: \"backend\" for back, \"frontend\" for front).",
|
|
341
359
|
"name": "target",
|
|
@@ -374,32 +392,27 @@
|
|
|
374
392
|
"index.js"
|
|
375
393
|
]
|
|
376
394
|
},
|
|
377
|
-
"
|
|
395
|
+
"keys": {
|
|
378
396
|
"aliases": [],
|
|
379
|
-
"args": {
|
|
380
|
-
|
|
381
|
-
"description": "Name of the project to create.",
|
|
382
|
-
"name": "name",
|
|
383
|
-
"required": true
|
|
384
|
-
}
|
|
385
|
-
},
|
|
386
|
-
"description": "Create a new Aurora monorepo project.",
|
|
397
|
+
"args": {},
|
|
398
|
+
"description": "Generate RSA private key and public key (defaults to ./backend/.keys)",
|
|
387
399
|
"examples": [
|
|
388
|
-
"<%= config.bin %>
|
|
389
|
-
"<%= config.bin %>
|
|
400
|
+
"<%= config.bin %> <%= command.id %>",
|
|
401
|
+
"<%= config.bin %> <%= command.id %> --out ./keys"
|
|
390
402
|
],
|
|
391
403
|
"flags": {
|
|
392
|
-
"
|
|
393
|
-
"char": "
|
|
394
|
-
"description": "
|
|
395
|
-
"name": "
|
|
396
|
-
"
|
|
397
|
-
"
|
|
404
|
+
"out": {
|
|
405
|
+
"char": "o",
|
|
406
|
+
"description": "Output directory for the key files (default: backend/.keys)",
|
|
407
|
+
"name": "out",
|
|
408
|
+
"hasDynamicHelp": false,
|
|
409
|
+
"multiple": false,
|
|
410
|
+
"type": "option"
|
|
398
411
|
}
|
|
399
412
|
},
|
|
400
413
|
"hasDynamicHelp": false,
|
|
401
414
|
"hiddenAliases": [],
|
|
402
|
-
"id": "
|
|
415
|
+
"id": "keys",
|
|
403
416
|
"pluginAlias": "@aurorajs.dev/catalyst-cli",
|
|
404
417
|
"pluginName": "@aurorajs.dev/catalyst-cli",
|
|
405
418
|
"pluginType": "core",
|
|
@@ -409,31 +422,36 @@
|
|
|
409
422
|
"relativePath": [
|
|
410
423
|
"dist",
|
|
411
424
|
"commands",
|
|
412
|
-
"
|
|
425
|
+
"keys",
|
|
413
426
|
"index.js"
|
|
414
427
|
]
|
|
415
428
|
},
|
|
416
|
-
"
|
|
429
|
+
"new": {
|
|
417
430
|
"aliases": [],
|
|
418
|
-
"args": {
|
|
419
|
-
|
|
431
|
+
"args": {
|
|
432
|
+
"name": {
|
|
433
|
+
"description": "Name of the project to create.",
|
|
434
|
+
"name": "name",
|
|
435
|
+
"required": true
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
"description": "Create a new Aurora monorepo project.",
|
|
420
439
|
"examples": [
|
|
421
|
-
"<%= config.bin %>
|
|
422
|
-
"<%= config.bin %>
|
|
440
|
+
"<%= config.bin %> new my-project",
|
|
441
|
+
"<%= config.bin %> new my-project --force"
|
|
423
442
|
],
|
|
424
443
|
"flags": {
|
|
425
|
-
"
|
|
426
|
-
"char": "
|
|
427
|
-
"description": "
|
|
428
|
-
"name": "
|
|
429
|
-
"
|
|
430
|
-
"
|
|
431
|
-
"type": "option"
|
|
444
|
+
"force": {
|
|
445
|
+
"char": "f",
|
|
446
|
+
"description": "Overwrite existing directory.",
|
|
447
|
+
"name": "force",
|
|
448
|
+
"allowNo": false,
|
|
449
|
+
"type": "boolean"
|
|
432
450
|
}
|
|
433
451
|
},
|
|
434
452
|
"hasDynamicHelp": false,
|
|
435
453
|
"hiddenAliases": [],
|
|
436
|
-
"id": "
|
|
454
|
+
"id": "new",
|
|
437
455
|
"pluginAlias": "@aurorajs.dev/catalyst-cli",
|
|
438
456
|
"pluginName": "@aurorajs.dev/catalyst-cli",
|
|
439
457
|
"pluginType": "core",
|
|
@@ -443,7 +461,7 @@
|
|
|
443
461
|
"relativePath": [
|
|
444
462
|
"dist",
|
|
445
463
|
"commands",
|
|
446
|
-
"
|
|
464
|
+
"new",
|
|
447
465
|
"index.js"
|
|
448
466
|
]
|
|
449
467
|
},
|
|
@@ -1080,5 +1098,5 @@
|
|
|
1080
1098
|
]
|
|
1081
1099
|
}
|
|
1082
1100
|
},
|
|
1083
|
-
"version": "1.0.
|
|
1101
|
+
"version": "1.0.2"
|
|
1084
1102
|
}
|
package/package.json
CHANGED