adonia 0.1.0
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 +64 -0
- package/build/adonis_peers.d.ts +14 -0
- package/build/adonis_peers.js +56 -0
- package/build/cli.d.ts +2 -0
- package/build/cli.js +111 -0
- package/build/commands/add.d.ts +18 -0
- package/build/commands/add.js +246 -0
- package/build/commands/check.d.ts +4 -0
- package/build/commands/check.js +159 -0
- package/build/commands/diff.d.ts +2 -0
- package/build/commands/diff.js +74 -0
- package/build/commands/init.d.ts +6 -0
- package/build/commands/init.js +124 -0
- package/build/commands/list.d.ts +2 -0
- package/build/commands/list.js +34 -0
- package/build/config_merge.d.ts +10 -0
- package/build/config_merge.js +71 -0
- package/build/constants_merge.d.ts +4 -0
- package/build/constants_merge.js +85 -0
- package/build/copy.d.ts +8 -0
- package/build/copy.js +33 -0
- package/build/host.d.ts +15 -0
- package/build/host.js +85 -0
- package/build/registry/index.d.ts +26 -0
- package/build/registry/index.js +116 -0
- package/build/registry/plan.d.ts +53 -0
- package/build/registry/plan.js +159 -0
- package/build/schema.d.ts +136 -0
- package/build/schema.js +58 -0
- package/build/types.d.ts +21 -0
- package/build/types.js +1 -0
- package/build/wire.d.ts +16 -0
- package/build/wire.js +162 -0
- package/package.json +51 -0
- package/registry/README.md +86 -0
- package/registry/account/contracts/avatar_storage.ts +7 -0
- package/registry/account/contracts/index.ts +7 -0
- package/registry/account/contracts/types.ts +23 -0
- package/registry/account/events.ts +14 -0
- package/registry/account/module.json +31 -0
- package/registry/account/options.ts +11 -0
- package/registry/account/service.ts +155 -0
- package/registry/account/stubs/adapters/account.ts +21 -0
- package/registry/account/stubs/adapters/drive_avatar_storage.ts +15 -0
- package/registry/account/stubs/controllers/account_controller.ts +190 -0
- package/registry/account/stubs/routes/account.ts +53 -0
- package/registry/account/stubs/validators/account.ts +37 -0
- package/registry/account/tests/account_service.spec.ts +132 -0
- package/registry/account/tests/fakes/memory_avatar_storage.ts +12 -0
- package/registry/api/envelope.ts +66 -0
- package/registry/api/exception.ts +38 -0
- package/registry/api/exception_handler.ts +54 -0
- package/registry/api/module.json +27 -0
- package/registry/api/provider.ts +129 -0
- package/registry/api/service.ts +88 -0
- package/registry/api/tests/emit_safe.spec.ts +107 -0
- package/registry/api/tests/respond.spec.ts +40 -0
- package/registry/auth/contracts/index.ts +18 -0
- package/registry/auth/contracts/password_reset_store.ts +21 -0
- package/registry/auth/contracts/session_manager.ts +19 -0
- package/registry/auth/contracts/types.ts +32 -0
- package/registry/auth/events.ts +21 -0
- package/registry/auth/module.json +45 -0
- package/registry/auth/options.ts +23 -0
- package/registry/auth/service.ts +150 -0
- package/registry/auth/stubs/adapters/auth.ts +21 -0
- package/registry/auth/stubs/adapters/lucid_access_token_session.ts +33 -0
- package/registry/auth/stubs/adapters/lucid_password_reset_store.ts +68 -0
- package/registry/auth/stubs/adapters/lucid_user_store.ts +86 -0
- package/registry/auth/stubs/adapters/map_user.ts +19 -0
- package/registry/auth/stubs/controllers/auth_controller.ts +126 -0
- package/registry/auth/stubs/migrations/add_pending_email_to_users_table.ts +17 -0
- package/registry/auth/stubs/migrations/create_password_resets_table.ts +27 -0
- package/registry/auth/stubs/migrations/create_users_table.ts +31 -0
- package/registry/auth/stubs/models/password_reset.ts +29 -0
- package/registry/auth/stubs/models/user.ts +99 -0
- package/registry/auth/stubs/providers/vine_provider.ts +43 -0
- package/registry/auth/stubs/routes/auth.ts +36 -0
- package/registry/auth/stubs/start/limiter.ts +17 -0
- package/registry/auth/stubs/validators/auth.ts +48 -0
- package/registry/auth/stubs/validators/rules/exists.ts +21 -0
- package/registry/auth/stubs/validators/rules/unique.ts +21 -0
- package/registry/auth/stubs/validators/rules/valid_password.ts +20 -0
- package/registry/auth/stubs/validators/rules/valid_token.ts +27 -0
- package/registry/auth/stubs/validators/vine.d.ts +10 -0
- package/registry/auth/tests/auth_service.spec.ts +145 -0
- package/registry/auth/tests/auth_update_password.spec.ts +99 -0
- package/registry/auth/tests/fakes/index.ts +3 -0
- package/registry/auth/tests/fakes/memory_password_reset_store.ts +71 -0
- package/registry/auth/tests/fakes/memory_session_manager.ts +23 -0
- package/registry/auth/tests/fakes/memory_user_store.ts +89 -0
- package/registry/constants/exceptions.ts +64 -0
- package/registry/constants/index.ts +12 -0
- package/registry/constants/responseCodes.ts +87 -0
- package/registry/contracts/index.ts +5 -0
- package/registry/contracts/user.ts +78 -0
- package/registry/creem/contracts/creem_client.ts +34 -0
- package/registry/creem/contracts/index.ts +8 -0
- package/registry/creem/contracts/types.ts +30 -0
- package/registry/creem/events.ts +9 -0
- package/registry/creem/module.json +28 -0
- package/registry/creem/service.ts +87 -0
- package/registry/creem/stubs/adapters/creem.ts +23 -0
- package/registry/creem/stubs/adapters/creem_io_client.ts +70 -0
- package/registry/creem/tests/fakes/fake_creem_client.ts +68 -0
- package/registry/notification/contracts/app_notification_store.ts +8 -0
- package/registry/notification/contracts/index.ts +9 -0
- package/registry/notification/contracts/mail_transport.ts +7 -0
- package/registry/notification/contracts/types.ts +32 -0
- package/registry/notification/module.json +23 -0
- package/registry/notification/options.ts +11 -0
- package/registry/notification/service.ts +103 -0
- package/registry/notification/stubs/adapters/adonis_mail_transport.ts +15 -0
- package/registry/notification/stubs/adapters/lucid_app_notification_store.ts +21 -0
- package/registry/notification/stubs/adapters/notification.ts +36 -0
- package/registry/notification/stubs/migrations/create_notifications_table.ts +23 -0
- package/registry/notification/stubs/models/app_notification.ts +45 -0
- package/registry/notification/tests/fakes/memory_app_notification_store.ts +19 -0
- package/registry/notification/tests/fakes/memory_mail_transport.ts +16 -0
- package/registry/notification/tests/notification_service.spec.ts +63 -0
- package/registry/registry.json +4 -0
- package/registry/subscription/contracts/index.ts +9 -0
- package/registry/subscription/contracts/subscription_store.ts +15 -0
- package/registry/subscription/contracts/types.ts +39 -0
- package/registry/subscription/events.ts +13 -0
- package/registry/subscription/module.json +31 -0
- package/registry/subscription/options.ts +14 -0
- package/registry/subscription/service.ts +140 -0
- package/registry/subscription/stubs/adapters/lucid_subscription_store.ts +69 -0
- package/registry/subscription/stubs/adapters/subscription.ts +21 -0
- package/registry/subscription/stubs/migrations/create_subscriptions_table.ts +26 -0
- package/registry/subscription/stubs/migrations/subscriptions_user_creem_unique.ts +19 -0
- package/registry/subscription/stubs/models/subscription.ts +45 -0
- package/registry/subscription/tests/billing_service.spec.ts +189 -0
- package/registry/subscription/tests/fakes/memory_subscription_store.ts +63 -0
- package/registry/types/api.ts +41 -0
- package/registry/types/constants.ts +7 -0
- package/registry/types/index.ts +6 -0
- package/registry/types/models.ts +32 -0
- package/schema/adonia.schema.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# `@adonia` CLI (`packages/modules`)
|
|
2
|
+
|
|
3
|
+
npm package name: `adonia`. Bin: `adonia`.
|
|
4
|
+
|
|
5
|
+
In this monorepo, module sources are authored in
|
|
6
|
+
`apps/adonis-api-stater/modules/`. Sync before pack/test:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm run sync-registry -w adonia
|
|
10
|
+
npm test -w adonia
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Host install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx adonia@latest init --wire
|
|
17
|
+
npx adonia@latest add auth
|
|
18
|
+
# optional host scaffolding:
|
|
19
|
+
npx adonia@latest add auth --with-adapters
|
|
20
|
+
npx adonia@latest add auth --with-stubs
|
|
21
|
+
npx adonia@latest add auth --with-routes --wire-routes
|
|
22
|
+
# or full kit:
|
|
23
|
+
npx adonia@latest init --scaffold
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Commands
|
|
27
|
+
|
|
28
|
+
| Command | Role |
|
|
29
|
+
|---------|------|
|
|
30
|
+
| `init` / `init --wire` | `adonia.json` + core; optional host wiring |
|
|
31
|
+
| `init --scaffold` | Core + all feature modules with stubs and routes (implies `--wire`) |
|
|
32
|
+
| `add [names…]` | Copy module files under `modules/`, merge config (no app stubs by default) |
|
|
33
|
+
| `list` / `diff` / `check` | Discovery, drift, validation |
|
|
34
|
+
|
|
35
|
+
### `add` scaffolding flags
|
|
36
|
+
|
|
37
|
+
| Flag | Effect |
|
|
38
|
+
|------|--------|
|
|
39
|
+
| `--with-models` | Copy `stubs/models/**` |
|
|
40
|
+
| `--with-migrations` | Copy `stubs/migrations/**` |
|
|
41
|
+
| `--with-controllers` | Copy `stubs/controllers/**` (implies `--with-adapters`) |
|
|
42
|
+
| `--with-validators` | Copy `stubs/validators/**` and `stubs/providers/**` |
|
|
43
|
+
| `--with-adapters` | Copy Lucid/SDK adapters → `app/adapters/` |
|
|
44
|
+
| `--with-stubs` | Models, migrations, controllers, validators, adapters, Vine provider, limiter stub (not routes) |
|
|
45
|
+
| `--with-routes` | Copy `stubs/routes/**` → `start/routes/` and `stubs/start/**` → `start/` |
|
|
46
|
+
| `--wire-routes` | Append `import './routes/<name>.js'` in `start/routes.ts` |
|
|
47
|
+
| `--with-tests` | Copy colocated module tests |
|
|
48
|
+
|
|
49
|
+
`diff` accepts the same `--with-*` flags; without them it diffs the
|
|
50
|
+
module tree only.
|
|
51
|
+
|
|
52
|
+
Adonis kits used by stubs (`@adonisjs/mail`, `@adonisjs/drive`,
|
|
53
|
+
`@adonisjs/limiter`) are not installed by `adonia add`. The CLI assumes
|
|
54
|
+
they are already configured, or `add` / `check` prints
|
|
55
|
+
`node ace add @adonisjs/<pkg>`.
|
|
56
|
+
|
|
57
|
+
Shared flags: `--yes`, `--dry-run`, `--cwd`, `--registry`, `--overwrite`.
|
|
58
|
+
|
|
59
|
+
## Developing in the monorepo
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node packages/modules/build/cli.js add auth \
|
|
63
|
+
--registry apps/adonis-api-stater/modules
|
|
64
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface AdonisPeer {
|
|
2
|
+
pkg: string;
|
|
3
|
+
reason: string;
|
|
4
|
+
/** Host-relative path that means this Adonis kit is actually in use. */
|
|
5
|
+
whenFile?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Adonis kits we assume are configured, or we tell the host to `ace add`. */
|
|
8
|
+
export declare const ADONIS_PEERS: Record<string, AdonisPeer[]>;
|
|
9
|
+
export declare function hostPackageNames(cwd: string): Set<string>;
|
|
10
|
+
export declare function missingAdonisPeers(cwd: string, moduleName: string, opts?: {
|
|
11
|
+
plannedFiles?: Iterable<string>;
|
|
12
|
+
}): AdonisPeer[];
|
|
13
|
+
export declare function aceAddHint(pkg: string): string;
|
|
14
|
+
export declare function formatMissingPeer(peer: AdonisPeer): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Adonis kits we assume are configured, or we tell the host to `ace add`. */
|
|
4
|
+
export const ADONIS_PEERS = {
|
|
5
|
+
auth: [
|
|
6
|
+
{
|
|
7
|
+
pkg: '@adonisjs/limiter',
|
|
8
|
+
reason: 'rate-limit auth routes',
|
|
9
|
+
whenFile: 'start/limiter.ts',
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
account: [
|
|
13
|
+
{
|
|
14
|
+
pkg: '@adonisjs/drive',
|
|
15
|
+
reason: 'avatar file cleanup',
|
|
16
|
+
whenFile: 'app/adapters/drive_avatar_storage.ts',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
notification: [
|
|
20
|
+
{
|
|
21
|
+
pkg: '@adonisjs/mail',
|
|
22
|
+
reason: 'email notification channel',
|
|
23
|
+
whenFile: 'app/adapters/adonis_mail_transport.ts',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
export function hostPackageNames(cwd) {
|
|
28
|
+
const pkgPath = join(cwd, 'package.json');
|
|
29
|
+
if (!existsSync(pkgPath))
|
|
30
|
+
return new Set();
|
|
31
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
32
|
+
return new Set([
|
|
33
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
34
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
export function missingAdonisPeers(cwd, moduleName, opts) {
|
|
38
|
+
const have = hostPackageNames(cwd);
|
|
39
|
+
const planned = new Set(opts?.plannedFiles ?? []);
|
|
40
|
+
return (ADONIS_PEERS[moduleName] ?? []).filter((peer) => {
|
|
41
|
+
if (have.has(peer.pkg))
|
|
42
|
+
return false;
|
|
43
|
+
if (peer.whenFile) {
|
|
44
|
+
const dest = join(cwd, peer.whenFile);
|
|
45
|
+
if (!planned.has(dest) && !existsSync(dest))
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function aceAddHint(pkg) {
|
|
52
|
+
return `node ace add ${pkg}`;
|
|
53
|
+
}
|
|
54
|
+
export function formatMissingPeer(peer) {
|
|
55
|
+
return `Missing ${peer.pkg} (${peer.reason}). Run: ${aceAddHint(peer.pkg)}`;
|
|
56
|
+
}
|
package/build/cli.d.ts
ADDED
package/build/cli.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { runAdd } from './commands/add.js';
|
|
5
|
+
import { runCheck } from './commands/check.js';
|
|
6
|
+
import { runDiff } from './commands/diff.js';
|
|
7
|
+
import { runInit } from './commands/init.js';
|
|
8
|
+
import { runList } from './commands/list.js';
|
|
9
|
+
import { packageVersion } from './host.js';
|
|
10
|
+
const program = new Command();
|
|
11
|
+
program
|
|
12
|
+
.name('adonia')
|
|
13
|
+
.description('Adonia modules for AdonisJS')
|
|
14
|
+
.version(packageVersion());
|
|
15
|
+
function shared(cmd) {
|
|
16
|
+
const opts = cmd.optsWithGlobals();
|
|
17
|
+
return {
|
|
18
|
+
cwd: resolve(opts.cwd ?? process.cwd()),
|
|
19
|
+
yes: Boolean(opts.yes),
|
|
20
|
+
dryRun: Boolean(opts.dryRun),
|
|
21
|
+
registry: opts.registry,
|
|
22
|
+
overwrite: Boolean(opts.overwrite),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function scaffoldFromOpts(opts) {
|
|
26
|
+
return {
|
|
27
|
+
withModels: Boolean(opts.withModels),
|
|
28
|
+
withMigrations: Boolean(opts.withMigrations),
|
|
29
|
+
withControllers: Boolean(opts.withControllers),
|
|
30
|
+
withValidators: Boolean(opts.withValidators),
|
|
31
|
+
withAdapters: Boolean(opts.withAdapters),
|
|
32
|
+
withStubs: Boolean(opts.withStubs),
|
|
33
|
+
withRoutes: Boolean(opts.withRoutes),
|
|
34
|
+
wireRoutes: Boolean(opts.wireRoutes),
|
|
35
|
+
withTests: Boolean(opts.withTests),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function addScaffoldOptions(cmd) {
|
|
39
|
+
return cmd
|
|
40
|
+
.option('--with-models', 'copy stubs/models into the host', false)
|
|
41
|
+
.option('--with-migrations', 'copy stubs/migrations into the host', false)
|
|
42
|
+
.option('--with-controllers', 'copy stubs/controllers (implies --with-adapters)', false)
|
|
43
|
+
.option('--with-validators', 'copy stubs/validators and Vine provider into the host', false)
|
|
44
|
+
.option('--with-adapters', 'copy Lucid/SDK adapters into app/adapters/', false)
|
|
45
|
+
.option('--with-stubs', 'copy models, migrations, controllers, validators, adapters, Vine provider, and limiter stub', false)
|
|
46
|
+
.option('--with-routes', 'copy stubs/routes and start/limiter into the host', false)
|
|
47
|
+
.option('--wire-routes', 'append imports in start/routes.ts for module route files', false)
|
|
48
|
+
.option('--with-tests', 'copy colocated module tests', false);
|
|
49
|
+
}
|
|
50
|
+
program
|
|
51
|
+
.option('--cwd <dir>', 'host app directory', process.cwd())
|
|
52
|
+
.option('-y, --yes', 'skip prompts', false)
|
|
53
|
+
.option('--dry-run', 'print plan without writing', false)
|
|
54
|
+
.option('--registry <dir>', 'local registry path (fixture testing)')
|
|
55
|
+
.option('--overwrite', 'replace differing host files', false);
|
|
56
|
+
program
|
|
57
|
+
.command('init')
|
|
58
|
+
.description('Write adonia.json and install shared API core')
|
|
59
|
+
.option('--wire', 'apply host aliases / provider / exception handler', false)
|
|
60
|
+
.option('--skip-core', 'do not copy api + core packages', false)
|
|
61
|
+
.option('--scaffold', 'install all feature modules with stubs + routes (implies --wire)', false)
|
|
62
|
+
.action(async (_args, cmd) => {
|
|
63
|
+
const flags = shared(cmd);
|
|
64
|
+
const opts = cmd.opts();
|
|
65
|
+
await runInit({
|
|
66
|
+
...flags,
|
|
67
|
+
wire: Boolean(opts.wire),
|
|
68
|
+
skipCore: Boolean(opts.skipCore),
|
|
69
|
+
scaffold: Boolean(opts.scaffold),
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
addScaffoldOptions(program
|
|
73
|
+
.command('add')
|
|
74
|
+
.description('Add one or more modules from the registry')
|
|
75
|
+
.argument('[names...]', 'module names')
|
|
76
|
+
.option('--overwrite', 'replace differing host files', false)).action(async (names, _opts, cmd) => {
|
|
77
|
+
const flags = shared(cmd);
|
|
78
|
+
const opts = cmd.opts();
|
|
79
|
+
await runAdd(names, {
|
|
80
|
+
...flags,
|
|
81
|
+
overwrite: flags.overwrite || Boolean(opts.overwrite),
|
|
82
|
+
...scaffoldFromOpts(opts),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
program
|
|
86
|
+
.command('list')
|
|
87
|
+
.description('List registry modules and install status')
|
|
88
|
+
.action(async (_args, cmd) => {
|
|
89
|
+
await runList(shared(cmd));
|
|
90
|
+
});
|
|
91
|
+
addScaffoldOptions(program
|
|
92
|
+
.command('diff')
|
|
93
|
+
.description('Diff host files against the registry (module files; stubs only with --with-*)')
|
|
94
|
+
.argument('[name]', 'module name')).action(async (name, _opts, cmd) => {
|
|
95
|
+
const flags = shared(cmd);
|
|
96
|
+
const opts = cmd.opts();
|
|
97
|
+
await runDiff(name, { ...flags, ...scaffoldFromOpts(opts) });
|
|
98
|
+
});
|
|
99
|
+
program
|
|
100
|
+
.command('check')
|
|
101
|
+
.description('Validate host modules install')
|
|
102
|
+
.option('--strict', 'treat warnings as failures', false)
|
|
103
|
+
.action(async (_args, cmd) => {
|
|
104
|
+
const flags = shared(cmd);
|
|
105
|
+
const opts = cmd.opts();
|
|
106
|
+
await runCheck({ ...flags, strict: Boolean(opts.strict) });
|
|
107
|
+
});
|
|
108
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
109
|
+
console.error(err instanceof Error ? err.message : err);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Registry } from '../registry/index.js';
|
|
2
|
+
import { type PlannedCopy } from '../registry/plan.js';
|
|
3
|
+
import type { HostModulesConfig } from '../schema.js';
|
|
4
|
+
import type { ScaffoldFlags, SharedFlags } from '../types.js';
|
|
5
|
+
export interface AddFlags extends SharedFlags, ScaffoldFlags {
|
|
6
|
+
/** Hide the post-install next-steps footer (used by init core). */
|
|
7
|
+
suppressNextSteps?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function runAdd(names: string[], flags: AddFlags): Promise<void>;
|
|
10
|
+
export declare function installModules(opts: {
|
|
11
|
+
names: string[];
|
|
12
|
+
flags: AddFlags;
|
|
13
|
+
config: HostModulesConfig;
|
|
14
|
+
registry: Registry;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
conflicts: PlannedCopy[];
|
|
17
|
+
wireErrors: string[];
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { mergeModulesConfig } from '../config_merge.js';
|
|
5
|
+
import { mergeConstantsCatalogs } from '../constants_merge.js';
|
|
6
|
+
import { detectPackageManager, loadHostConfig, writeHostConfig } from '../host.js';
|
|
7
|
+
import { expandDependencies, loadManifest, moduleDir, resolveRegistry, } from '../registry/index.js';
|
|
8
|
+
import { anyAppStubsSelected, describePlan, executePlan, hostRouteFile, manifestHasRouteStubs, planModuleFiles, planStubs, planTests, resolveStubSelect, } from '../registry/plan.js';
|
|
9
|
+
import { formatMissingPeer, missingAdonisPeers } from '../adonis_peers.js';
|
|
10
|
+
import { wireModuleRoute } from '../wire.js';
|
|
11
|
+
export async function runAdd(names, flags) {
|
|
12
|
+
if (!names.length) {
|
|
13
|
+
console.error('Usage: adonia add <name…>');
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let config;
|
|
18
|
+
try {
|
|
19
|
+
config = loadHostConfig(flags.cwd);
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
console.error(err instanceof Error ? err.message : err);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let registry;
|
|
27
|
+
try {
|
|
28
|
+
registry = await resolveRegistry({
|
|
29
|
+
registryFlag: flags.registry,
|
|
30
|
+
hostRegistry: config.registry,
|
|
31
|
+
ref: config.ref,
|
|
32
|
+
cwd: flags.cwd,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
console.error(err instanceof Error ? err.message : err);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let ordered;
|
|
41
|
+
try {
|
|
42
|
+
ordered = expandDependencies(registry, names, new Set(config.installed), flags.overwrite);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
console.error(err instanceof Error ? err.message : err);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!ordered.length) {
|
|
50
|
+
console.log('Nothing to install (already in adonia.json installed). Use --overwrite to recopy.');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
console.log(`Plan: ${ordered.join(' → ')}`);
|
|
54
|
+
if (!flags.yes && !flags.dryRun) {
|
|
55
|
+
console.log('(continuing; pass --yes to silence)');
|
|
56
|
+
}
|
|
57
|
+
const result = await installModules({ names: ordered, flags, config, registry });
|
|
58
|
+
if (result.conflicts.length) {
|
|
59
|
+
console.error('\nConflicts (use --overwrite to replace). Not recorded in adonia.json\n' +
|
|
60
|
+
'installed until all planned files for the module copy cleanly.');
|
|
61
|
+
for (const c of result.conflicts) {
|
|
62
|
+
console.error(` ${describePlan(c, flags.cwd)}`);
|
|
63
|
+
}
|
|
64
|
+
process.exitCode = 1;
|
|
65
|
+
}
|
|
66
|
+
if (result.wireErrors.length) {
|
|
67
|
+
for (const e of result.wireErrors)
|
|
68
|
+
console.error(e);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function installModules(opts) {
|
|
73
|
+
const { names, flags, registry } = opts;
|
|
74
|
+
let config = opts.config;
|
|
75
|
+
const allConflicts = [];
|
|
76
|
+
const wireErrors = [];
|
|
77
|
+
const warnings = [];
|
|
78
|
+
const npmDeps = new Map();
|
|
79
|
+
const plannedDests = new Set();
|
|
80
|
+
const stubSelect = resolveStubSelect(flags);
|
|
81
|
+
const requestedNames = new Set(names);
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const manifest = loadManifest(registry, name);
|
|
84
|
+
const plans = [];
|
|
85
|
+
plans.push(...planModuleFiles(registry, name, manifest, config, flags.cwd));
|
|
86
|
+
if (stubSelect) {
|
|
87
|
+
plans.push(...planStubs(registry, name, manifest, config, flags.cwd, warnings, stubSelect));
|
|
88
|
+
}
|
|
89
|
+
if (flags.withTests) {
|
|
90
|
+
plans.push(...planTests(registry, name, config, flags.cwd));
|
|
91
|
+
}
|
|
92
|
+
for (const p of plans)
|
|
93
|
+
plannedDests.add(p.dest);
|
|
94
|
+
if (flags.dryRun) {
|
|
95
|
+
for (const p of plans) {
|
|
96
|
+
console.log(`would process ${describePlan(p, flags.cwd)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const { results, conflicts: moduleConflicts } = executePlan(plans, {
|
|
100
|
+
overwrite: flags.overwrite,
|
|
101
|
+
dryRun: flags.dryRun,
|
|
102
|
+
});
|
|
103
|
+
allConflicts.push(...moduleConflicts);
|
|
104
|
+
for (const { plan, result } of results) {
|
|
105
|
+
if (result === 'copied' || result === 'would-copy') {
|
|
106
|
+
console.log(`${result}: ${describePlan(plan, flags.cwd)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const [pkg, version] of Object.entries(manifest.dependencies)) {
|
|
110
|
+
npmDeps.set(pkg, version);
|
|
111
|
+
}
|
|
112
|
+
// Constants merge after core is present: re-merge registry constants into host
|
|
113
|
+
if (name !== 'constants' && !flags.dryRun) {
|
|
114
|
+
const hostConstants = join(flags.cwd, config.paths.modules, 'constants');
|
|
115
|
+
const regConstants = join(moduleDir(registry, 'constants'));
|
|
116
|
+
if (existsSync(hostConstants) && existsSync(regConstants)) {
|
|
117
|
+
warnings.push(...mergeConstantsCatalogs(regConstants, hostConstants, flags.dryRun));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Merge config/modules.ts for feature modules with events
|
|
121
|
+
if (manifest.events.length &&
|
|
122
|
+
name !== 'api' &&
|
|
123
|
+
!['types', 'constants', 'contracts'].includes(name)) {
|
|
124
|
+
const configPath = join(flags.cwd, 'config/modules.ts');
|
|
125
|
+
const merged = mergeModulesConfig(configPath, name, manifest.description, manifest.events, flags.dryRun);
|
|
126
|
+
if (!merged.ok) {
|
|
127
|
+
console.warn(`Could not merge config/modules.ts for ${name}. Add manually:\n${merged.snippet}`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
console.log(`${flags.dryRun ? 'Would update' : 'Updated'} config/modules.ts (${name})`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Env hints
|
|
134
|
+
if (manifest.env.length) {
|
|
135
|
+
const missing = missingEnvKeys(flags.cwd, manifest.env);
|
|
136
|
+
if (missing.length) {
|
|
137
|
+
console.log(`Missing env keys for ${name} (not written): ${missing.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (flags.wireRoutes && manifestHasRouteStubs(manifest)) {
|
|
141
|
+
const routeDest = hostRouteFile(flags.cwd, name);
|
|
142
|
+
const plannedRoute = plans.some((p) => p.dest === routeDest);
|
|
143
|
+
const haveRoute = existsSync(routeDest) || plannedRoute;
|
|
144
|
+
if (!haveRoute) {
|
|
145
|
+
wireErrors.push(`Cannot --wire-routes for "${name}": missing start/routes/${name}.ts. ` +
|
|
146
|
+
`Pass --with-routes first, or create the file.`);
|
|
147
|
+
}
|
|
148
|
+
else if (flags.dryRun && !existsSync(routeDest)) {
|
|
149
|
+
console.log(`Would wire routes: start/routes.ts → ./routes/${name}.js`);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const wired = wireModuleRoute(flags.cwd, name, flags.dryRun);
|
|
153
|
+
if (!wired.ok) {
|
|
154
|
+
wireErrors.push(wired.error);
|
|
155
|
+
}
|
|
156
|
+
else if (wired.actions.length) {
|
|
157
|
+
console.log(`${flags.dryRun ? 'Would wire' : 'Wired'} routes: ${wired.actions.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!flags.dryRun && moduleConflicts.length === 0 && !config.installed.includes(name)) {
|
|
162
|
+
config = {
|
|
163
|
+
...config,
|
|
164
|
+
installed: [...config.installed, name],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!flags.dryRun) {
|
|
169
|
+
writeHostConfig(flags.cwd, config);
|
|
170
|
+
}
|
|
171
|
+
if (npmDeps.size && !flags.dryRun) {
|
|
172
|
+
installNpmDeps(flags.cwd, npmDeps);
|
|
173
|
+
}
|
|
174
|
+
else if (npmDeps.size && flags.dryRun) {
|
|
175
|
+
console.log(`Would install npm deps: ${[...npmDeps.keys()].join(', ')}`);
|
|
176
|
+
}
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
for (const peer of missingAdonisPeers(flags.cwd, name, { plannedFiles: plannedDests })) {
|
|
179
|
+
warnings.push(formatMissingPeer(peer));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for (const w of warnings) {
|
|
183
|
+
if (w)
|
|
184
|
+
console.warn(w);
|
|
185
|
+
}
|
|
186
|
+
printNextSteps(flags, stubSelect, [...requestedNames][0] ?? 'auth');
|
|
187
|
+
return { conflicts: allConflicts, wireErrors };
|
|
188
|
+
}
|
|
189
|
+
function printNextSteps(flags, stubSelect, exampleName) {
|
|
190
|
+
if (flags.suppressNextSteps)
|
|
191
|
+
return;
|
|
192
|
+
console.log('\nNext steps (not run by the CLI):');
|
|
193
|
+
if (!anyAppStubsSelected(stubSelect) && !stubSelect?.routes) {
|
|
194
|
+
console.log('Models/migrations/controllers/validators/adapters/routes were not copied.');
|
|
195
|
+
console.log(` adonia add ${exampleName} --with-stubs`);
|
|
196
|
+
console.log(` adonia add ${exampleName} --with-adapters`);
|
|
197
|
+
console.log(` adonia add ${exampleName} --with-routes --wire-routes`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
if (stubSelect?.routes && !flags.wireRoutes) {
|
|
201
|
+
console.log(` adonia add ${exampleName} --wire-routes # mount start/routes/${exampleName}.ts`);
|
|
202
|
+
}
|
|
203
|
+
if (stubSelect?.providers) {
|
|
204
|
+
console.log(' Register providers/vine_provider.ts in adonisrc.ts providers.');
|
|
205
|
+
}
|
|
206
|
+
if (stubSelect?.start) {
|
|
207
|
+
console.log(' After limiter is installed, uncomment .use(authLimiter) / .use(accountLimiter) on sensitive routes.');
|
|
208
|
+
}
|
|
209
|
+
console.log(' 1. Run migrations: node ace migration:run');
|
|
210
|
+
console.log(' 2. Listen for module events as needed');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function missingEnvKeys(cwd, keys) {
|
|
214
|
+
const sources = [];
|
|
215
|
+
for (const f of ['.env', '.env.example', 'start/env.ts']) {
|
|
216
|
+
const p = join(cwd, f);
|
|
217
|
+
if (existsSync(p))
|
|
218
|
+
sources.push(readFileSync(p, 'utf8'));
|
|
219
|
+
}
|
|
220
|
+
const blob = sources.join('\n');
|
|
221
|
+
return keys.filter((k) => !blob.includes(k));
|
|
222
|
+
}
|
|
223
|
+
function installNpmDeps(cwd, deps) {
|
|
224
|
+
const pkgPath = join(cwd, 'package.json');
|
|
225
|
+
if (!existsSync(pkgPath))
|
|
226
|
+
return;
|
|
227
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
228
|
+
const have = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
229
|
+
const missing = [...deps.entries()].filter(([name]) => !have[name]);
|
|
230
|
+
if (!missing.length) {
|
|
231
|
+
console.log('npm dependencies already satisfied.');
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const pm = detectPackageManager(cwd);
|
|
235
|
+
const specs = missing.map(([name, version]) => `${name}@${version}`);
|
|
236
|
+
console.log(`Installing with ${pm}: ${specs.join(' ')}`);
|
|
237
|
+
const args = pm === 'yarn'
|
|
238
|
+
? ['add', ...specs]
|
|
239
|
+
: pm === 'pnpm'
|
|
240
|
+
? ['add', ...specs]
|
|
241
|
+
: ['install', ...specs];
|
|
242
|
+
const result = spawnSync(pm, args, { cwd, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
243
|
+
if (result.status !== 0) {
|
|
244
|
+
console.warn(`Package install exited ${result.status}; install manually: ${specs.join(' ')}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { formatMissingPeer, missingAdonisPeers } from '../adonis_peers.js';
|
|
4
|
+
import { loadHostConfig } from '../host.js';
|
|
5
|
+
import { loadManifest, resolveRegistry } from '../registry/index.js';
|
|
6
|
+
import { CORE_PACKAGES } from '../schema.js';
|
|
7
|
+
import { REQUIRED_IMPORTS } from '../wire.js';
|
|
8
|
+
// Reserved manifest fields (not checked yet): hostTests, hostFacingExceptions.
|
|
9
|
+
export async function runCheck(flags) {
|
|
10
|
+
let config;
|
|
11
|
+
try {
|
|
12
|
+
config = loadHostConfig(flags.cwd);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
console.error(err instanceof Error ? err.message : err);
|
|
16
|
+
process.exitCode = 1;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const errors = [];
|
|
20
|
+
const warnings = [];
|
|
21
|
+
const installed = new Set(config.installed);
|
|
22
|
+
for (const [key, rel] of Object.entries(config.paths)) {
|
|
23
|
+
const p = join(flags.cwd, rel);
|
|
24
|
+
if (!existsSync(p)) {
|
|
25
|
+
// paths may not exist until first add — warn for empty dirs
|
|
26
|
+
warnings.push(`path.${key} missing: ${rel}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
let registry;
|
|
30
|
+
try {
|
|
31
|
+
registry = await resolveRegistry({
|
|
32
|
+
registryFlag: flags.registry,
|
|
33
|
+
hostRegistry: config.registry,
|
|
34
|
+
ref: config.ref,
|
|
35
|
+
cwd: flags.cwd,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(err instanceof Error ? err.message : err);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const name of config.installed) {
|
|
44
|
+
const folder = join(flags.cwd, config.paths.modules, name);
|
|
45
|
+
if (!existsSync(folder)) {
|
|
46
|
+
errors.push(`installed module folder missing: ${folder}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const manifestPath = join(folder, 'module.json');
|
|
50
|
+
if (existsSync(manifestPath)) {
|
|
51
|
+
try {
|
|
52
|
+
const local = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
53
|
+
if (local.name && local.name !== name) {
|
|
54
|
+
errors.push(`module.json name mismatch in ${name}: ${local.name}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
warnings.push(`could not parse ${manifestPath}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const manifest = loadManifest(registry, name);
|
|
63
|
+
const requiredDeps = [...manifest.registryDependencies];
|
|
64
|
+
if (name === 'api') {
|
|
65
|
+
for (const core of CORE_PACKAGES) {
|
|
66
|
+
if (!requiredDeps.includes(core))
|
|
67
|
+
requiredDeps.push(core);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const dep of requiredDeps) {
|
|
71
|
+
if (!installed.has(dep)) {
|
|
72
|
+
errors.push(`installed module "${name}" requires "${dep}" (not in adonia.json installed)`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const peer of manifest.peerModels) {
|
|
76
|
+
const file = peerModelToPath(peer, config.paths.models, flags.cwd);
|
|
77
|
+
if (!existsSync(file)) {
|
|
78
|
+
errors.push(`peerModel missing: ${peer} → ${file}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (manifest.events.length) {
|
|
82
|
+
const configPath = join(flags.cwd, 'config/modules.ts');
|
|
83
|
+
if (!existsSync(configPath)) {
|
|
84
|
+
warnings.push(`config/modules.ts missing (events for ${name})`);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const text = readFileSync(configPath, 'utf8');
|
|
88
|
+
for (const ev of manifest.events) {
|
|
89
|
+
if (!text.includes(ev)) {
|
|
90
|
+
warnings.push(`config/modules.ts missing event ${ev} for ${name}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const peer of missingAdonisPeers(flags.cwd, name)) {
|
|
96
|
+
warnings.push(formatMissingPeer(peer));
|
|
97
|
+
}
|
|
98
|
+
if (manifest.configKeys.length) {
|
|
99
|
+
const configPath = join(flags.cwd, 'config/modules.ts');
|
|
100
|
+
if (!existsSync(configPath)) {
|
|
101
|
+
warnings.push(`config/modules.ts missing (configKeys for ${name})`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const text = readFileSync(configPath, 'utf8');
|
|
105
|
+
for (const key of manifest.configKeys) {
|
|
106
|
+
const leaf = key.includes('.') ? key.slice(key.lastIndexOf('.') + 1) : key;
|
|
107
|
+
if (!hasConfigKey(text, leaf)) {
|
|
108
|
+
warnings.push(`config/modules.ts missing key ${leaf} (${key}) for ${name}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
warnings.push(err instanceof Error ? err.message : String(err));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// aliases
|
|
119
|
+
const pkgPath = join(flags.cwd, 'package.json');
|
|
120
|
+
if (existsSync(pkgPath)) {
|
|
121
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
122
|
+
const imports = pkg.imports ?? {};
|
|
123
|
+
for (const key of Object.keys(REQUIRED_IMPORTS)) {
|
|
124
|
+
if (!(key in imports)) {
|
|
125
|
+
warnings.push(`package.json imports missing ${key}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const adonisrc = join(flags.cwd, 'adonisrc.ts');
|
|
130
|
+
if (existsSync(adonisrc) && !readFileSync(adonisrc, 'utf8').includes('#modules/api/provider')) {
|
|
131
|
+
warnings.push('adonisrc.ts missing #modules/api/provider (run init --wire)');
|
|
132
|
+
}
|
|
133
|
+
const kernel = join(flags.cwd, 'start/kernel.ts');
|
|
134
|
+
if (existsSync(kernel) && !readFileSync(kernel, 'utf8').includes('#modules/api/exception_handler')) {
|
|
135
|
+
warnings.push('start/kernel.ts missing exception_handler (run init --wire)');
|
|
136
|
+
}
|
|
137
|
+
for (const w of warnings)
|
|
138
|
+
console.warn(`warn: ${w}`);
|
|
139
|
+
for (const e of errors)
|
|
140
|
+
console.error(`error: ${e}`);
|
|
141
|
+
if (errors.length)
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
else if (flags.strict && warnings.length)
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
else if (!errors.length)
|
|
146
|
+
console.log('check passed' + (warnings.length ? ` (${warnings.length} warnings)` : ''));
|
|
147
|
+
}
|
|
148
|
+
function hasConfigKey(text, leaf) {
|
|
149
|
+
const re = new RegExp(`\\b${escapeRegExp(leaf)}\\s*:`);
|
|
150
|
+
return re.test(text);
|
|
151
|
+
}
|
|
152
|
+
function escapeRegExp(s) {
|
|
153
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
154
|
+
}
|
|
155
|
+
function peerModelToPath(alias, modelsPath, cwd) {
|
|
156
|
+
// #models/user → app/models/user.ts
|
|
157
|
+
const name = alias.replace(/^#models\//, '');
|
|
158
|
+
return join(cwd, modelsPath, `${name}.ts`);
|
|
159
|
+
}
|