create-astram-app 0.1.2 → 0.2.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/dist/add-frontend.js +107 -0
- package/dist/add-frontend.js.map +1 -0
- package/dist/index.js +132 -33
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/template/CLAUDE.md +51 -0
- package/template/README.md +43 -44
- package/template/_gitignore +6 -0
- package/template/frontend/FRONTEND.md +92 -0
- package/template/frontend/README.md +16 -0
- package/template/frontend/_env.example +6 -0
- package/template/frontend/_gitignore +9 -0
- package/template/frontend/eslint.config.js +71 -0
- package/template/frontend/index.html +13 -0
- package/template/frontend/package.json +52 -0
- package/template/frontend/public/astram-logo-full.png +0 -0
- package/template/frontend/public/astram-logo-white.png +0 -0
- package/template/frontend/public/favicon.png +0 -0
- package/template/frontend/src/App.jsx +167 -0
- package/template/frontend/src/auth/AuthContext.jsx +338 -0
- package/template/frontend/src/components/ConfirmDialog.jsx +92 -0
- package/template/frontend/src/components/EntityFormModal.jsx +154 -0
- package/template/frontend/src/components/EntityList.jsx +480 -0
- package/template/frontend/src/components/PageHeader.jsx +54 -0
- package/template/frontend/src/components/access/CollapsibleGroup.jsx +34 -0
- package/template/frontend/src/components/ui/alert-dialog.tsx +175 -0
- package/template/frontend/src/components/ui/alert.tsx +83 -0
- package/template/frontend/src/components/ui/avatar.tsx +51 -0
- package/template/frontend/src/components/ui/badge.tsx +56 -0
- package/template/frontend/src/components/ui/button.tsx +67 -0
- package/template/frontend/src/components/ui/card.tsx +92 -0
- package/template/frontend/src/components/ui/checkbox.tsx +30 -0
- package/template/frontend/src/components/ui/date-field.tsx +47 -0
- package/template/frontend/src/components/ui/dialog.tsx +214 -0
- package/template/frontend/src/components/ui/dropdown-menu.tsx +255 -0
- package/template/frontend/src/components/ui/input-otp.tsx +81 -0
- package/template/frontend/src/components/ui/input.tsx +21 -0
- package/template/frontend/src/components/ui/label.tsx +46 -0
- package/template/frontend/src/components/ui/searchable-select.tsx +459 -0
- package/template/frontend/src/components/ui/select.tsx +199 -0
- package/template/frontend/src/components/ui/separator.tsx +26 -0
- package/template/frontend/src/components/ui/sheet.tsx +139 -0
- package/template/frontend/src/components/ui/skeleton.tsx +13 -0
- package/template/frontend/src/components/ui/sonner.tsx +23 -0
- package/template/frontend/src/components/ui/table.tsx +181 -0
- package/template/frontend/src/components/ui/tooltip.tsx +59 -0
- package/template/frontend/src/config/branding.js +80 -0
- package/template/frontend/src/hooks/useMobile.tsx +21 -0
- package/template/frontend/src/index.css +265 -0
- package/template/frontend/src/lib/utils.js +7 -0
- package/template/frontend/src/main.jsx +27 -0
- package/template/frontend/src/pages/Dashboard.jsx +127 -0
- package/template/frontend/src/pages/ForgotPassword.jsx +104 -0
- package/template/frontend/src/pages/Login.jsx +393 -0
- package/template/frontend/src/pages/ResetPassword.jsx +159 -0
- package/template/frontend/src/pages/access/AccessForm.jsx +416 -0
- package/template/frontend/src/pages/access/AccessPage.jsx +282 -0
- package/template/frontend/src/pages/access/RoleActionsMenu.jsx +42 -0
- package/template/frontend/src/pages/account/AccountLayout.jsx +229 -0
- package/template/frontend/src/pages/account/ChangePasswordPage.jsx +235 -0
- package/template/frontend/src/pages/account/ProfilePage.jsx +122 -0
- package/template/frontend/src/pages/account/ReenrollPage.jsx +98 -0
- package/template/frontend/src/pages/account/TwoFactorPage.jsx +833 -0
- package/template/frontend/src/pages/audit/AuditPage.jsx +268 -0
- package/template/frontend/src/pages/companies/CompaniesPage.jsx +232 -0
- package/template/frontend/src/pages/companies/CompanyForm.jsx +127 -0
- package/template/frontend/src/pages/roles/RoleForm.jsx +99 -0
- package/template/frontend/src/pages/roles/RolesPage.jsx +207 -0
- package/template/frontend/src/pages/users/AssignmentsEditor.jsx +193 -0
- package/template/frontend/src/pages/users/MembershipCard.jsx +303 -0
- package/template/frontend/src/pages/users/PostingRow.jsx +264 -0
- package/template/frontend/src/pages/users/UserForm.jsx +167 -0
- package/template/frontend/src/pages/users/UsersPage.jsx +232 -0
- package/template/frontend/src/platform/httpClient.js +125 -0
- package/template/frontend/src/platform/returnPathStore.js +43 -0
- package/template/frontend/src/platform/sessionStore.js +29 -0
- package/template/frontend/src/platform/shieldTransport.js +172 -0
- package/template/frontend/src/platform/shieldTransport.test.mjs +245 -0
- package/template/frontend/src/shell/AppShell.jsx +237 -0
- package/template/frontend/src/shell/Sidebar.jsx +257 -0
- package/template/frontend/src/shell/nav.js +90 -0
- package/template/frontend/src/theme/ThemeProvider.jsx +14 -0
- package/template/frontend/src/theme/ThemeToggle.jsx +44 -0
- package/template/frontend/tsconfig.json +22 -0
- package/template/frontend/vite.config.js +30 -0
- package/template/server/_env.example +45 -0
- package/template/server/eslint.config.js +95 -0
- package/template/server/package.json +49 -0
- package/template/server/scripts/dev.mjs +51 -0
- package/template/server/scripts/sync-astram-guide.mjs +63 -0
- package/template/_env.example +0 -33
- package/template/package.json +0 -37
- /package/template/{src → server/src}/app.module.ts +0 -0
- /package/template/{src → server/src}/features/_gitkeep +0 -0
- /package/template/{src → server/src}/health/demo.controller.ts +0 -0
- /package/template/{src → server/src}/health/health.controller.ts +0 -0
- /package/template/{src → server/src}/main.ts +0 -0
- /package/template/{test → server/test}/enforcement.e2e.spec.ts +0 -0
- /package/template/{tsconfig.build.json → server/tsconfig.build.json} +0 -0
- /package/template/{tsconfig.json → server/tsconfig.json} +0 -0
- /package/template/{vitest.config.ts → server/vitest.config.ts} +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `astram-add-frontend` — additive frontend installer for an EXISTING Astram app
|
|
4
|
+
* (ADR-0023 §3, Task B8).
|
|
5
|
+
*
|
|
6
|
+
* cd my-existing-app
|
|
7
|
+
* npx astram-add-frontend
|
|
8
|
+
*
|
|
9
|
+
* This is the SECOND delivery path for `template/frontend/` (the Vite/React
|
|
10
|
+
* identity UI) alongside `create-astram-app <name> --with-frontend` (Task B7,
|
|
11
|
+
* which emits it at scaffold time for a BRAND NEW app). `astram-add-frontend`
|
|
12
|
+
* targets an app that was scaffolded earlier WITHOUT `--with-frontend`, or any
|
|
13
|
+
* existing directory a developer wants to bolt the identity UI onto later.
|
|
14
|
+
*
|
|
15
|
+
* Copies `template/frontend/` → `<cwd>/frontend/` using the EXACT same
|
|
16
|
+
* `copyTemplate` skip-if-exists semantics as the main scaffolder (imported from
|
|
17
|
+
* `./index.js`, not re-implemented): existing files are NEVER overwritten, so
|
|
18
|
+
* re-running this bin after the developer has edited `frontend/` is always safe
|
|
19
|
+
* — it only adds files that are still missing (e.g. a newer template picked up
|
|
20
|
+
* a file this app doesn't have yet).
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
23
|
+
import { basename, join, resolve } from 'node:path';
|
|
24
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
|
+
import { copyTemplate, templateDir } from './index.js';
|
|
26
|
+
/**
|
|
27
|
+
* The app name substituted into `__APP_NAME__` tokens in the copied frontend
|
|
28
|
+
* files (`package.json` name, `<title>`, comments — see `template/frontend/`).
|
|
29
|
+
* Resolved from the CURRENT DIRECTORY'S OWN `package.json` `name` field when
|
|
30
|
+
* present (this is an EXISTING app being extended, not a fresh scaffold), else
|
|
31
|
+
* falls back to the directory's basename.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveAppName(cwd) {
|
|
34
|
+
const pkgPath = join(cwd, 'package.json');
|
|
35
|
+
if (existsSync(pkgPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
38
|
+
if (typeof pkg.name === 'string' && pkg.name.trim().length > 0) {
|
|
39
|
+
return pkg.name.trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Malformed/unreadable package.json — fall through to the directory name
|
|
44
|
+
// rather than failing the whole add-frontend run over a cosmetic token.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return basename(cwd);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Add `frontend/` to the app rooted at `cwd` (default `process.cwd()`). Returns
|
|
51
|
+
* the target dir + copy report. Exported for tests (which pass an explicit
|
|
52
|
+
* `cwd`), mirroring `scaffold()` in `index.ts`.
|
|
53
|
+
*/
|
|
54
|
+
export function addFrontend(cwd = process.cwd()) {
|
|
55
|
+
const appName = resolveAppName(cwd);
|
|
56
|
+
const targetDir = resolve(cwd, 'frontend');
|
|
57
|
+
const report = { written: [], skipped: [] };
|
|
58
|
+
copyTemplate(join(templateDir(), 'frontend'), targetDir, appName, report, 'frontend');
|
|
59
|
+
return { targetDir, report };
|
|
60
|
+
}
|
|
61
|
+
/** Entry point. Returns a process exit code (0 ok, non-zero on error). */
|
|
62
|
+
export function main() {
|
|
63
|
+
let result;
|
|
64
|
+
try {
|
|
65
|
+
result = addFrontend(process.cwd());
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error(`astram-add-frontend: ${err.message}`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const { report } = result;
|
|
72
|
+
console.log('\nAdded frontend/ (Vite/React identity UI) to this app.');
|
|
73
|
+
console.log(` ${report.written.length} file(s) written.`);
|
|
74
|
+
if (report.skipped.length > 0) {
|
|
75
|
+
console.log(` ${report.skipped.length} existing file(s) left untouched (never-clobber): ${report.skipped.join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('\nNext steps:');
|
|
78
|
+
console.log(' cd frontend');
|
|
79
|
+
console.log(' npm install');
|
|
80
|
+
console.log(' npm run dev # boots the identity UI (login, session)');
|
|
81
|
+
console.log('');
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
// Run ONLY when this module is the process entry point (i.e. executed as the
|
|
85
|
+
// CLI), NOT when imported by a test. Identical symlink-safe realpath check to
|
|
86
|
+
// `index.ts`'s `isInvokedAsCli()`: `npx`/npm run a `bin` through a SYMLINK, so
|
|
87
|
+
// `process.argv[1]` is the symlink path, not this real dist file — resolve
|
|
88
|
+
// (realpath) before comparing, or the CLI silently no-ops under npx. When
|
|
89
|
+
// imported by a test, argv[1] is the test runner, resolving to a different
|
|
90
|
+
// real file → correctly skips.
|
|
91
|
+
function isInvokedAsCli() {
|
|
92
|
+
const entry = process.argv[1];
|
|
93
|
+
if (entry === undefined)
|
|
94
|
+
return false;
|
|
95
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
96
|
+
try {
|
|
97
|
+
return realpathSync(entry) === realpathSync(thisFile);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// If either path can't be realpath'd, fall back to a direct compare.
|
|
101
|
+
return pathToFileURL(entry).href === import.meta.url;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (isInvokedAsCli()) {
|
|
105
|
+
process.exitCode = main();
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=add-frontend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-frontend.js","sourceRoot":"","sources":["../src/add-frontend.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* `create-astram-app` — the Astram app scaffolder (ADR-0023 §3).
|
|
4
4
|
*
|
|
5
|
-
* npx create-astram-app <name>
|
|
5
|
+
* npx create-astram-app <name> # server/ only
|
|
6
|
+
* npx create-astram-app <name> --with-frontend # + frontend/ (identity UI)
|
|
6
7
|
*
|
|
7
8
|
* Emits a NEW consumer app directory that depends on the published, immutable
|
|
8
9
|
* framework packages (`@astram/core`, `@astram/guard`, `@astram/sdk`) via real
|
|
@@ -21,12 +22,20 @@
|
|
|
21
22
|
* Reference skeleton: `examples/example-backend/` in the astram-framework repo.
|
|
22
23
|
*/
|
|
23
24
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
24
|
-
import { dirname, join, resolve } from 'node:path';
|
|
25
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
25
26
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
27
|
/** Token substituted with the app name at copy time (in package.json name, README title, …). */
|
|
27
28
|
const APP_NAME_TOKEN = '__APP_NAME__';
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Directory (inside the published package) that holds the emitted app template.
|
|
31
|
+
* Resolved relative to THIS file's own compiled location, NOT the caller's —
|
|
32
|
+
* safe to import from a sibling bin (e.g. `add-frontend.ts`, Task B8) because
|
|
33
|
+
* both compile into the SAME `dist/` directory, so `dist/index.js/../template`
|
|
34
|
+
* and `dist/add-frontend.js/../template` are identical. Exported so sibling
|
|
35
|
+
* bins reuse this exact path-math instead of duplicating (and risking drift
|
|
36
|
+
* from) it.
|
|
37
|
+
*/
|
|
38
|
+
export function templateDir() {
|
|
30
39
|
// dist/index.js → ../template (bundled via the package's `files` field).
|
|
31
40
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
32
41
|
return resolve(here, '..', 'template');
|
|
@@ -59,15 +68,42 @@ export function validateAppName(name) {
|
|
|
59
68
|
return null;
|
|
60
69
|
}
|
|
61
70
|
const USAGE = [
|
|
62
|
-
'Usage: create-astram-app <name>',
|
|
71
|
+
'Usage: create-astram-app <name> [--with-frontend]',
|
|
63
72
|
'',
|
|
64
73
|
'Scaffolds a new Astram consumer app that depends on the published framework',
|
|
65
74
|
'(@astram/core, @astram/guard, @astram/sdk) and enforces deny-by-default from',
|
|
66
75
|
'the first boot.',
|
|
67
76
|
'',
|
|
77
|
+
' --with-frontend Also scaffold frontend/ (Vite/React identity UI).',
|
|
78
|
+
' Without this flag, only server/ is scaffolded.',
|
|
79
|
+
'',
|
|
68
80
|
'Example:',
|
|
69
81
|
' npx create-astram-app my-app',
|
|
82
|
+
' npx create-astram-app my-app --with-frontend',
|
|
70
83
|
].join('\n');
|
|
84
|
+
/**
|
|
85
|
+
* Copy a SINGLE template file `srcPath` → `destPath` (never a directory),
|
|
86
|
+
* substituting {@link APP_NAME_TOKEN} in text file CONTENTS, and NEVER
|
|
87
|
+
* overwriting a file that already exists at the destination (spec §3.1 —
|
|
88
|
+
* never clobber developer-authored files). `rel` is the path relative to the
|
|
89
|
+
* template root, recorded in `report.written`/`report.skipped`.
|
|
90
|
+
*/
|
|
91
|
+
function copyTemplateFile(srcPath, destPath, appName, report, rel) {
|
|
92
|
+
if (existsSync(destPath)) {
|
|
93
|
+
// Never-clobber: leave the developer's file exactly as-is.
|
|
94
|
+
report.skipped.push(rel);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const raw = readFileSync(srcPath);
|
|
98
|
+
if (isTextFile(basename(srcPath))) {
|
|
99
|
+
const substituted = raw.toString('utf8').split(APP_NAME_TOKEN).join(appName);
|
|
100
|
+
writeFileSync(destPath, substituted, 'utf8');
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
cpSync(srcPath, destPath);
|
|
104
|
+
}
|
|
105
|
+
report.written.push(rel);
|
|
106
|
+
}
|
|
71
107
|
/**
|
|
72
108
|
* Recursively copy `srcDir` → `destDir`, substituting {@link APP_NAME_TOKEN} in
|
|
73
109
|
* text file CONTENTS, and NEVER overwriting a file that already exists at the
|
|
@@ -75,8 +111,14 @@ const USAGE = [
|
|
|
75
111
|
* are created as needed; only missing files are written.
|
|
76
112
|
*
|
|
77
113
|
* `relBase` accumulates the path relative to the template root for reporting.
|
|
114
|
+
*
|
|
115
|
+
* Exported so sibling bins (e.g. `add-frontend.ts`, Task B8) reuse this EXACT
|
|
116
|
+
* copy semantics — skip-if-exists / never-clobber / __APP_NAME__ substitution —
|
|
117
|
+
* for a single-subtree copy (`template/frontend` → `<cwd>/frontend`) instead of
|
|
118
|
+
* a full app scaffold, without re-implementing (and risking drift from) the
|
|
119
|
+
* never-clobber contract.
|
|
78
120
|
*/
|
|
79
|
-
function copyTemplate(srcDir, destDir, appName, report, relBase = '') {
|
|
121
|
+
export function copyTemplate(srcDir, destDir, appName, report, relBase = '') {
|
|
80
122
|
mkdirSync(destDir, { recursive: true });
|
|
81
123
|
for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
|
|
82
124
|
const srcPath = join(srcDir, entry.name);
|
|
@@ -90,31 +132,30 @@ function copyTemplate(srcDir, destDir, appName, report, relBase = '') {
|
|
|
90
132
|
copyTemplate(srcPath, destPath, appName, report, rel);
|
|
91
133
|
continue;
|
|
92
134
|
}
|
|
93
|
-
|
|
94
|
-
// Never-clobber: leave the developer's file exactly as-is.
|
|
95
|
-
report.skipped.push(rel);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const raw = readFileSync(srcPath);
|
|
99
|
-
if (isTextFile(entry.name)) {
|
|
100
|
-
const substituted = raw.toString('utf8').split(APP_NAME_TOKEN).join(appName);
|
|
101
|
-
writeFileSync(destPath, substituted, 'utf8');
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
cpSync(srcPath, destPath);
|
|
105
|
-
}
|
|
106
|
-
report.written.push(rel);
|
|
135
|
+
copyTemplateFile(srcPath, destPath, appName, report, rel);
|
|
107
136
|
}
|
|
108
137
|
}
|
|
109
138
|
/** Text extensions that get token substitution + utf8 handling. */
|
|
110
139
|
function isTextFile(fileName) {
|
|
111
140
|
return /\.(json|ts|js|mjs|cjs|md|txt|example|yml|yaml|gitignore)$/i.test(fileName) || fileName.startsWith('_');
|
|
112
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Names of the template's top-level SUBTREES that are copied separately (not
|
|
144
|
+
* via the generic root-level pass below), because each is independently
|
|
145
|
+
* gated: `server/` is always emitted, `frontend/` only with `--with-frontend`.
|
|
146
|
+
*/
|
|
147
|
+
const TEMPLATE_SUBTREES = ['server', 'frontend'];
|
|
113
148
|
/**
|
|
114
149
|
* Scaffold `appName` into `<cwd>/<appName>`. Returns the copy report + resolved
|
|
115
150
|
* target dir. Exported for tests (which pass an explicit `cwd`).
|
|
151
|
+
*
|
|
152
|
+
* `withFrontend` gates the `template/frontend/` → `<app>/frontend/` copy — the
|
|
153
|
+
* scaffolded app is server-only by default; `--with-frontend` (CLI) or
|
|
154
|
+
* `withFrontend: true` (programmatic) also emits the Vite/React identity UI.
|
|
155
|
+
* `server/` and the app-root files (`.astram/guide/`, `CLAUDE.md`, `.gitignore`,
|
|
156
|
+
* `README.md`) are unconditional either way — unchanged from prior behavior.
|
|
116
157
|
*/
|
|
117
|
-
export function scaffold(appName, cwd = process.cwd()) {
|
|
158
|
+
export function scaffold(appName, cwd = process.cwd(), withFrontend = false) {
|
|
118
159
|
const targetDir = resolve(cwd, appName);
|
|
119
160
|
const report = { written: [], skipped: [] };
|
|
120
161
|
if (existsSync(targetDir)) {
|
|
@@ -125,12 +166,56 @@ export function scaffold(appName, cwd = process.cwd()) {
|
|
|
125
166
|
// Existing dir is allowed — copyTemplate skips any file that already exists,
|
|
126
167
|
// so developer-authored docs/, .astram/guide/, README, etc. survive untouched.
|
|
127
168
|
}
|
|
128
|
-
|
|
169
|
+
const template = templateDir();
|
|
170
|
+
// The generic per-directory copyTemplate() calls always mkdirSync their own
|
|
171
|
+
// destDir first; the app root itself needs the same treatment since
|
|
172
|
+
// root-level FILES (e.g. CLAUDE.md) are written directly into targetDir
|
|
173
|
+
// below without going through copyTemplate.
|
|
174
|
+
mkdirSync(targetDir, { recursive: true });
|
|
175
|
+
// App-root files/dirs (.astram/guide/, CLAUDE.md, .gitignore, README.md, …) —
|
|
176
|
+
// everything directly under template/ EXCEPT the independently-gated
|
|
177
|
+
// server/ and frontend/ subtrees, which are copied below. Dispatch by dirent
|
|
178
|
+
// type: a directory recurses via copyTemplate; a plain file (e.g. CLAUDE.md)
|
|
179
|
+
// copies via copyTemplateFile directly — copyTemplate always treats its
|
|
180
|
+
// srcDir arg as a directory, so calling it on a FILE path throws ENOTDIR.
|
|
181
|
+
for (const entry of readdirSync(template, { withFileTypes: true })) {
|
|
182
|
+
if (TEMPLATE_SUBTREES.includes(entry.name))
|
|
183
|
+
continue;
|
|
184
|
+
const srcPath = join(template, entry.name);
|
|
185
|
+
const destName = entry.name.startsWith('_') ? `.${entry.name.slice(1)}` : entry.name;
|
|
186
|
+
const destPath = join(targetDir, destName);
|
|
187
|
+
if (entry.isDirectory()) {
|
|
188
|
+
copyTemplate(srcPath, destPath, appName, report, destName);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
copyTemplateFile(srcPath, destPath, appName, report, destName);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// server/ — always scaffolded (unchanged prior behavior).
|
|
195
|
+
copyTemplate(join(template, 'server'), join(targetDir, 'server'), appName, report, 'server');
|
|
196
|
+
// frontend/ — opt-in via --with-frontend (Task B7). Previously this was
|
|
197
|
+
// copied unconditionally as part of the wholesale template/ copy; it is now
|
|
198
|
+
// gated so a plain `create-astram-app <name>` stays server-only.
|
|
199
|
+
if (withFrontend) {
|
|
200
|
+
copyTemplate(join(template, 'frontend'), join(targetDir, 'frontend'), appName, report, 'frontend');
|
|
201
|
+
}
|
|
129
202
|
return { targetDir, report };
|
|
130
203
|
}
|
|
204
|
+
/** Long-form boolean flags this CLI recognizes (anywhere in argv). */
|
|
205
|
+
const WITH_FRONTEND_FLAG = '--with-frontend';
|
|
206
|
+
/**
|
|
207
|
+
* The app name is the first argv entry that is NOT a recognized `--flag` —
|
|
208
|
+
* robust to `--with-frontend` appearing before OR after the name
|
|
209
|
+
* (`create-astram-app --with-frontend my-app` and
|
|
210
|
+
* `create-astram-app my-app --with-frontend` both work).
|
|
211
|
+
*/
|
|
212
|
+
function parseAppName(argv) {
|
|
213
|
+
return argv.find((arg) => !arg.startsWith('--'));
|
|
214
|
+
}
|
|
131
215
|
/** Entry point. Returns a process exit code (0 ok, non-zero on error). */
|
|
132
216
|
export function main(argv = process.argv.slice(2)) {
|
|
133
|
-
const name = argv
|
|
217
|
+
const name = parseAppName(argv);
|
|
218
|
+
const withFrontend = argv.includes(WITH_FRONTEND_FLAG);
|
|
134
219
|
const validationError = validateAppName(name);
|
|
135
220
|
if (validationError) {
|
|
136
221
|
console.error(`create-astram-app: ${validationError}\n`);
|
|
@@ -140,7 +225,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
140
225
|
const appName = name.trim();
|
|
141
226
|
let report;
|
|
142
227
|
try {
|
|
143
|
-
({ report } = scaffold(appName));
|
|
228
|
+
({ report } = scaffold(appName, process.cwd(), withFrontend));
|
|
144
229
|
}
|
|
145
230
|
catch (err) {
|
|
146
231
|
console.error(`create-astram-app: ${err.message}`);
|
|
@@ -152,20 +237,34 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
152
237
|
if (report.skipped.length > 0) {
|
|
153
238
|
console.log(` ${report.skipped.length} existing file(s) left untouched (never-clobber): ${report.skipped.join(', ')}`);
|
|
154
239
|
}
|
|
155
|
-
console.log('\nWhat you got:');
|
|
156
|
-
console.log(' •
|
|
157
|
-
console.log(' default (ADR-0002) unless you classify it
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
console.log('\nWhat you got (a monorepo):');
|
|
241
|
+
console.log(' • server/ — a running, framework-ENFORCED NestJS API. Every route is denied');
|
|
242
|
+
console.log(' by default (ADR-0002) unless you classify it @Public()/@Classified().');
|
|
243
|
+
if (withFrontend) {
|
|
244
|
+
console.log(' • frontend/ — a Vite/React identity UI (login, session) wired to the API.');
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.log(' • frontend/ — NOT scaffolded (pass --with-frontend to also emit the Vite/React');
|
|
248
|
+
console.log(' identity UI). You can still add any UI framework yourself later.');
|
|
249
|
+
}
|
|
250
|
+
console.log(' • The framework installed in server/ as immutable dependencies (@astram/core,');
|
|
251
|
+
console.log(' @astram/guard, @astram/engine, @astram/sdk) — you never edit it; upgrade via');
|
|
252
|
+
console.log(' `npm update`.');
|
|
253
|
+
console.log(' • server/src/features/ — your editable API space. Add a feature by writing a');
|
|
254
|
+
console.log(' Nest module/controller there and registering it in server/src/app.module.ts.');
|
|
162
255
|
console.log(' • .astram/guide/ — your own, upgrade-safe project-rules space (ADR-0025).');
|
|
163
256
|
console.log('\n (A turnkey login/session boilerplate ships in a later framework release;');
|
|
164
257
|
console.log(' today the app gives you correct-by-default ENFORCEMENT out of the box.)');
|
|
165
258
|
console.log('\nNext steps:');
|
|
166
|
-
console.log(` cd ${appName}`);
|
|
259
|
+
console.log(` cd ${appName}/server`);
|
|
167
260
|
console.log(' npm install');
|
|
168
|
-
console.log(' npm run dev # boots the
|
|
261
|
+
console.log(' npm run dev # boots the API; GET /health → 200, unclassified → 403');
|
|
262
|
+
if (withFrontend) {
|
|
263
|
+
console.log('');
|
|
264
|
+
console.log(` cd ${appName}/frontend`);
|
|
265
|
+
console.log(' npm install');
|
|
266
|
+
console.log(' npm run dev # boots the identity UI (login, session)');
|
|
267
|
+
}
|
|
169
268
|
console.log('');
|
|
170
269
|
return 0;
|
|
171
270
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-astram-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Scaffolder for a new Astram app — emits a running, framework-enforced consumer project (empty src/features/ + deny-by-default enforcement) that installs the immutable @astram/core, @astram/guard, and @astram/sdk framework.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"create-astram-app": "dist/index.js"
|
|
8
|
+
"create-astram-app": "dist/index.js",
|
|
9
|
+
"astram-add-frontend": "dist/add-frontend.js"
|
|
9
10
|
},
|
|
10
11
|
"publishConfig": {
|
|
11
12
|
"access": "public"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# __APP_NAME__ — AI guide (entry)
|
|
2
|
+
|
|
3
|
+
This is the entry point for AI assistants (Claude Code) and teammates. It ties together
|
|
4
|
+
the **two-layer guide** (ADR-0025): the framework's rules (the floor) + this app's own
|
|
5
|
+
rules (on top). Read both before doing any work.
|
|
6
|
+
|
|
7
|
+
## 1. Framework rules — the FLOOR (framework-owned, refreshed on upgrade)
|
|
8
|
+
|
|
9
|
+
The Astram framework ships its hard rules + capabilities in the installed
|
|
10
|
+
`@astram/guide` package. They are synced into `.claude/rules/` (do NOT edit those
|
|
11
|
+
copies — they are regenerated from the framework package on every install/upgrade):
|
|
12
|
+
|
|
13
|
+
- **`.claude/rules/astram-framework-rules.md`** — the non-negotiable rules: never edit
|
|
14
|
+
the framework, all data through the RecordEngine pipe, classify every route,
|
|
15
|
+
**STOP-and-flag a gap** instead of working around it, fail closed, and — after any
|
|
16
|
+
framework upgrade — **re-read these rules first** (Rule 0).
|
|
17
|
+
- **`.claude/rules/astram-framework-capabilities.md`** — what the framework provides
|
|
18
|
+
this version (check here before building; if it's not there, it's a gap → flag it).
|
|
19
|
+
|
|
20
|
+
@./.claude/rules/astram-framework-rules.md
|
|
21
|
+
|
|
22
|
+
> These are a FLOOR: this app's own rules (below) may ADD to or TIGHTEN them, but never
|
|
23
|
+
> weaken them. The never-touch-core / never-bypass-enforcement rules are absolute.
|
|
24
|
+
|
|
25
|
+
## 2. Your project's rules — the project layer (yours, never touched by an upgrade)
|
|
26
|
+
|
|
27
|
+
Your project-specific rules, conventions, and the framework-gap log live in
|
|
28
|
+
**`.astram/guide/`** (git-tracked, developer-owned — a framework upgrade never touches
|
|
29
|
+
it):
|
|
30
|
+
|
|
31
|
+
- **`.astram/guide/README.md`** — what this layer is + how it relates to the framework.
|
|
32
|
+
- **`.astram/guide/gaps.md`** — the framework capability gaps you've flagged (create it
|
|
33
|
+
the first time you hit one — the gap-flag loop).
|
|
34
|
+
|
|
35
|
+
Add your own rules as markdown files in `.astram/guide/`.
|
|
36
|
+
|
|
37
|
+
## 3. Keeping the framework rules current (the upgrade flow)
|
|
38
|
+
|
|
39
|
+
The framework rules in `.claude/rules/` are a **generated mirror** of the installed
|
|
40
|
+
`@astram/guide` package. They refresh automatically:
|
|
41
|
+
|
|
42
|
+
- `server/`'s `postinstall` runs `sync-astram-guide` on every `npm install` / `npm
|
|
43
|
+
update`, copying the current framework rules into `.claude/rules/`.
|
|
44
|
+
- To refresh manually: `cd server && npm run guide:sync`.
|
|
45
|
+
- The synced framework files + `.claude/rules/.astram-guide-version` are **gitignored**
|
|
46
|
+
(they belong to the framework version, not your app). Your `.astram/guide/` layer is
|
|
47
|
+
git-tracked and yours.
|
|
48
|
+
|
|
49
|
+
**On a framework upgrade:** run `npm update` (or `npm install`) in `server/` — the
|
|
50
|
+
framework rules refresh to the new version, your `.astram/guide/` rules are untouched,
|
|
51
|
+
then re-read the refreshed `.claude/rules/astram-framework-rules.md` (Rule 0).
|
package/template/README.md
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
# __APP_NAME__
|
|
2
2
|
|
|
3
|
-
An **Astram consumer app**, scaffolded with `create-astram-app`.
|
|
4
|
-
|
|
3
|
+
An **Astram consumer app**, scaffolded with `create-astram-app`. A monorepo with a
|
|
4
|
+
`server/` (the API) and a `frontend/` (your UI). The API depends on the published,
|
|
5
|
+
immutable framework packages ([`@astram/core`](https://www.npmjs.com/package/@astram/core),
|
|
5
6
|
[`@astram/guard`](https://www.npmjs.com/package/@astram/guard),
|
|
7
|
+
[`@astram/engine`](https://www.npmjs.com/package/@astram/engine),
|
|
6
8
|
[`@astram/sdk`](https://www.npmjs.com/package/@astram/sdk)) and enforces
|
|
7
|
-
**deny-by-default** from the very first boot. You build features; the framework
|
|
8
|
-
|
|
9
|
+
**deny-by-default** from the very first boot. You build features; the framework lives
|
|
10
|
+
read-only in `server/node_modules` and upgrades via `npm update` — you never edit it.
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## Layout
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
__APP_NAME__/
|
|
16
|
+
server/ the API (NestJS + the Astram framework enforcement)
|
|
17
|
+
frontend/ your UI (empty — add Vite/React/Next/… and call the API)
|
|
18
|
+
.astram/ your project-owned, upgrade-safe AI/dev guide (ADR-0025)
|
|
19
|
+
docs/ your own docs (the scaffolder never clobbers this)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Run the API
|
|
11
23
|
|
|
12
24
|
```bash
|
|
25
|
+
cd server
|
|
13
26
|
npm install
|
|
14
|
-
npm run dev # boots the
|
|
27
|
+
npm run dev # boots the API (tsx watch) on http://localhost:3000
|
|
15
28
|
```
|
|
16
29
|
|
|
17
30
|
Then:
|
|
18
31
|
|
|
19
32
|
```bash
|
|
20
|
-
curl -i localhost:3000/health
|
|
21
|
-
curl -i localhost:3000/demo/public
|
|
33
|
+
curl -i localhost:3000/health # 200 — @Public health probe
|
|
34
|
+
curl -i localhost:3000/demo/public # 200 — explicit @Public() opt-out
|
|
22
35
|
curl -i localhost:3000/demo/unclassified # 403 — DENIED (no classification)
|
|
23
36
|
```
|
|
24
37
|
|
|
@@ -26,12 +39,13 @@ That last 403 is the whole point: **every route is denied unless you explicitly
|
|
|
26
39
|
classify it.** Forgetting to declare a route locks the door instead of silently
|
|
27
40
|
exposing it.
|
|
28
41
|
|
|
29
|
-
Other scripts: `npm run build` (→ `dist/`), `npm start` (run the build),
|
|
30
|
-
`npm test
|
|
42
|
+
Other `server/` scripts: `npm run build` (→ `dist/`), `npm start` (run the build),
|
|
43
|
+
`npm test`, `npm run lint`.
|
|
31
44
|
|
|
32
|
-
## How
|
|
45
|
+
## How the API wires the framework
|
|
33
46
|
|
|
34
|
-
`src/app.module.ts` imports the ONE registration seam — no local security
|
|
47
|
+
`server/src/app.module.ts` imports the ONE registration seam — no local security
|
|
48
|
+
wiring:
|
|
35
49
|
|
|
36
50
|
```ts
|
|
37
51
|
import { ShieldModule } from '@astram/guard';
|
|
@@ -45,45 +59,30 @@ export class AppModule {}
|
|
|
45
59
|
|
|
46
60
|
`ShieldModule.forRoot()` binds the global deny-by-default guard (ADR-0002), the
|
|
47
61
|
boot-pool assertion (ADR-0018), and the Core decision-authority token. You never
|
|
48
|
-
re-implement enforcement — you register it once and add your controllers.
|
|
62
|
+
re-implement enforcement — you register it once and add your controllers. Enable the
|
|
63
|
+
full auth surface (login/MFA/refresh/logout) with `ShieldModule.forRoot({ auth: true,
|
|
64
|
+
credentialResolver, decisionAuthority })`.
|
|
49
65
|
|
|
50
66
|
## Add a feature (the framework way)
|
|
51
67
|
|
|
52
|
-
1. Create `src/features/<your-feature>/` with a Nest module + controller(s).
|
|
53
|
-
2. Register it in `src/app.module.ts` (add the module to `imports`, or its
|
|
68
|
+
1. Create `server/src/features/<your-feature>/` with a Nest module + controller(s).
|
|
69
|
+
2. Register it in `server/src/app.module.ts` (add the module to `imports`, or its
|
|
54
70
|
controller to `controllers`).
|
|
55
|
-
3. **Classify every route:** `@Public()` for open routes, `@Classified(
|
|
56
|
-
guarded ones. Anything unclassified is denied (403) by design.
|
|
57
|
-
4.
|
|
71
|
+
3. **Classify every route:** `@Public()` for open routes, `@Classified({ typeKey,
|
|
72
|
+
action })` for guarded ones. Anything unclassified is denied (403) by design.
|
|
73
|
+
4. **Data access goes through the framework's RecordEngine** — never a raw DB pool.
|
|
74
|
+
The `server/eslint.config.js` wall rule fails the build on off-road DB access.
|
|
75
|
+
5. Add a test (see `server/test/` for the pattern).
|
|
58
76
|
|
|
59
|
-
`src/features/` starts empty — it's your space.
|
|
77
|
+
`server/src/features/` starts empty — it's your space.
|
|
60
78
|
|
|
61
|
-
##
|
|
79
|
+
## The frontend
|
|
62
80
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
later framework release — when it lands, you'll pick it up via `npm update` without
|
|
67
|
-
touching your `src/`.
|
|
68
|
-
|
|
69
|
-
## Layout
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
src/
|
|
73
|
-
main.ts HTTP bootstrap (thin) — loadAppConfig from @astram/guard
|
|
74
|
-
app.module.ts declares enforcement via ShieldModule.forRoot()
|
|
75
|
-
health/
|
|
76
|
-
health.controller.ts /health /ready /live (all @Public)
|
|
77
|
-
demo.controller.ts /demo/public (@Public) + /demo/unclassified (denied)
|
|
78
|
-
features/ EMPTY — your editable space
|
|
79
|
-
test/
|
|
80
|
-
enforcement.e2e.spec.ts boots the app; proves 200 health + 403 unclassified
|
|
81
|
-
.astram/guide/ YOUR project guide (upgrade-safe; never touched)
|
|
82
|
-
docs/ your own docs (the scaffolder never clobbers this)
|
|
83
|
-
```
|
|
81
|
+
`frontend/` is intentionally empty — Astram is a **backend security kernel**, so it
|
|
82
|
+
does not impose a UI framework. Add whatever you like and call the API. See
|
|
83
|
+
`frontend/README.md` for the auth endpoints your UI will consume.
|
|
84
84
|
|
|
85
85
|
## The `.astram/guide/` directory
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
`.astram/guide/README.md`.
|
|
87
|
+
Your **project-owned** AI/dev guide layer (ADR-0025) — framework upgrades never touch
|
|
88
|
+
it. Put your project's own rules and context there. See `.astram/guide/README.md`.
|
package/template/_gitignore
CHANGED
|
@@ -3,3 +3,9 @@ dist/
|
|
|
3
3
|
.env
|
|
4
4
|
*.log
|
|
5
5
|
.DS_Store
|
|
6
|
+
|
|
7
|
+
# The framework rules synced from @astram/guide (ADR-0025) — a GENERATED mirror of
|
|
8
|
+
# the installed framework version, refreshed on every install/upgrade. Not yours to
|
|
9
|
+
# edit or commit. (Your OWN rules live in .astram/guide/, which IS git-tracked.)
|
|
10
|
+
.claude/rules/astram-framework-*.md
|
|
11
|
+
.claude/rules/.astram-guide-version
|