create-astram-app 0.1.3 → 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 +122 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/template/frontend/FRONTEND.md +92 -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 +27 -15
- package/template/server/package.json +7 -4
|
@@ -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}`);
|
|
@@ -155,7 +240,13 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
155
240
|
console.log('\nWhat you got (a monorepo):');
|
|
156
241
|
console.log(' • server/ — a running, framework-ENFORCED NestJS API. Every route is denied');
|
|
157
242
|
console.log(' by default (ADR-0002) unless you classify it @Public()/@Classified().');
|
|
158
|
-
|
|
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
|
+
}
|
|
159
250
|
console.log(' • The framework installed in server/ as immutable dependencies (@astram/core,');
|
|
160
251
|
console.log(' @astram/guard, @astram/engine, @astram/sdk) — you never edit it; upgrade via');
|
|
161
252
|
console.log(' `npm update`.');
|
|
@@ -168,6 +259,12 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
168
259
|
console.log(` cd ${appName}/server`);
|
|
169
260
|
console.log(' npm install');
|
|
170
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
|
+
}
|
|
171
268
|
console.log('');
|
|
172
269
|
return 0;
|
|
173
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,92 @@
|
|
|
1
|
+
# __APP_NAME__ frontend
|
|
2
|
+
|
|
3
|
+
This is the Astram framework's **identity frontend** — login, session, users, roles,
|
|
4
|
+
companies, access, audit — scaffolded straight into your app. It is a real,
|
|
5
|
+
production-shaped Vite/React UI (shadcn/ui + Tailwind v4), not a placeholder.
|
|
6
|
+
|
|
7
|
+
## What this is
|
|
8
|
+
|
|
9
|
+
The files under `frontend/` are **yours** — you own and edit them like any other
|
|
10
|
+
part of your app. The framework does not manage or overwrite them once they're
|
|
11
|
+
here.
|
|
12
|
+
|
|
13
|
+
The actual auth/session/API **logic** does NOT live in these files — it comes from
|
|
14
|
+
two npm packages:
|
|
15
|
+
|
|
16
|
+
- **`@astram/frontend-core`** — auth flows, session handling, HTTP client wiring.
|
|
17
|
+
- **`@astram/sdk`** — typed calls against the framework API (users, roles,
|
|
18
|
+
companies, access, audit).
|
|
19
|
+
|
|
20
|
+
That split is what makes this model work: the **view** (these files: pages,
|
|
21
|
+
components, routing, styling) is scaffolded once and is fully yours to change;
|
|
22
|
+
the **logic** (auth correctness, new API surface, bug fixes) ships as ordinary
|
|
23
|
+
npm package updates you pull in with `npm update`, without touching a single file
|
|
24
|
+
in this folder.
|
|
25
|
+
|
|
26
|
+
## Run
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install
|
|
30
|
+
npm run dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- Opens the identity UI on **http://localhost:5173**.
|
|
34
|
+
- Log in at **`/login`**.
|
|
35
|
+
- The Vite dev server proxies **`/api`** to your backend on **`http://localhost:3000`**
|
|
36
|
+
(see `vite.config.js`) — the browser calls same-origin `/api/auth/login`, and the
|
|
37
|
+
proxy rewrites it to `/auth/login` on the server. Override the target with the
|
|
38
|
+
`API_URL` env var if your backend runs elsewhere.
|
|
39
|
+
|
|
40
|
+
Make sure the framework backend (`../server`) is running first — this UI has
|
|
41
|
+
nothing to talk to otherwise.
|
|
42
|
+
|
|
43
|
+
## Re-skin
|
|
44
|
+
|
|
45
|
+
You don't need to touch component code to make this look like your product:
|
|
46
|
+
|
|
47
|
+
- **Name / logo** — copy `.env.example` to `.env` and set:
|
|
48
|
+
- `VITE_APP_NAME` — shown in the header/title instead of the default.
|
|
49
|
+
- `VITE_APP_LOGO` — path/URL to your logo (light theme).
|
|
50
|
+
- `VITE_APP_LOGO_DARK` — path/URL to your logo (dark theme).
|
|
51
|
+
- `VITE_APP_TAGLINE` / `VITE_APP_SUB_TAGLINE` — login-screen copy.
|
|
52
|
+
- **Theme color** — edit the `--primary` / `--primary-foreground` oklch pair in
|
|
53
|
+
`src/index.css`: the `:root` block controls light mode, the `.dark` block
|
|
54
|
+
controls dark mode. Every primary button, focus ring, and accent in the UI
|
|
55
|
+
derives from this one token pair — change it once, it propagates everywhere.
|
|
56
|
+
|
|
57
|
+
Both of these are **additive, config-only** customizations — they don't modify
|
|
58
|
+
any scaffolded file, so a future framework/template update never conflicts with
|
|
59
|
+
your branding.
|
|
60
|
+
|
|
61
|
+
## Updates / no-clobber model
|
|
62
|
+
|
|
63
|
+
Two different things update on two different schedules, and it's important to
|
|
64
|
+
know which is which:
|
|
65
|
+
|
|
66
|
+
1. **Logic (`@astram/frontend-core`, `@astram/sdk`)** updates like any other npm
|
|
67
|
+
dependency — `npm update` pulls in bug fixes and new API methods for free. You
|
|
68
|
+
never edit these packages directly (they live in `node_modules`).
|
|
69
|
+
|
|
70
|
+
2. **View files (everything under `frontend/src/`, `frontend/index.html`, etc.)**
|
|
71
|
+
are yours. They were copied in ONCE, by either `create-astram-app --with-frontend`
|
|
72
|
+
at scaffold time or by `npx astram-add-frontend` afterwards. Nothing in the
|
|
73
|
+
framework reaches back into this folder to overwrite your edits — this file, a
|
|
74
|
+
page you've re-themed, a component you've customized, all survive indefinitely.
|
|
75
|
+
|
|
76
|
+
3. **Re-running `npx astram-add-frontend` is safe, any time.** It's additive and
|
|
77
|
+
idempotent: it only writes files that are still missing (e.g. the published
|
|
78
|
+
template gained a new page or component since you last ran it). Any file that
|
|
79
|
+
already exists in your `frontend/` — including one you've heavily edited — is
|
|
80
|
+
left completely untouched. It will never overwrite your work.
|
|
81
|
+
|
|
82
|
+
4. **Adopting an improved framework screen is a manual, deliberate step**, not an
|
|
83
|
+
automatic one — by design, so you're never surprised by a silent overwrite. If
|
|
84
|
+
a newer `create-astram-app` release improves a page you're using unmodified
|
|
85
|
+
(e.g. a better `Login.jsx`), diff your version against the current template
|
|
86
|
+
(published inside the `create-astram-app` npm package, under `template/frontend/`)
|
|
87
|
+
and cherry-pick the parts you want.
|
|
88
|
+
|
|
89
|
+
The practical effect: keep your branding in `.env` + the `--primary` token instead
|
|
90
|
+
of hand-editing component internals wherever you can — it keeps your view files
|
|
91
|
+
close to the upstream template, which makes adopting future improvements a small
|
|
92
|
+
diff instead of a merge conflict.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Frontend ESLint flat config (ESLint 9) for an Astram consumer app.
|
|
2
|
+
//
|
|
3
|
+
// React + hooks + fast-refresh rules over the JS/JSX source. The `lint` script
|
|
4
|
+
// (`eslint "src/**/*.{js,jsx}"`) resolves this file. The Vite build is the
|
|
5
|
+
// correctness gate; this catches unused vars, hook-dependency mistakes, and
|
|
6
|
+
// fast-refresh foot-guns. Uses the framework's `_`-prefixed-unused convention.
|
|
7
|
+
import js from '@eslint/js';
|
|
8
|
+
import globals from 'globals';
|
|
9
|
+
import react from 'eslint-plugin-react';
|
|
10
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
11
|
+
import reactRefresh from 'eslint-plugin-react-refresh';
|
|
12
|
+
|
|
13
|
+
export default [
|
|
14
|
+
{
|
|
15
|
+
ignores: ['node_modules/**', 'dist/**', 'coverage/**', 'eslint.config.js'],
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
js.configs.recommended,
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
files: ['src/**/*.{js,jsx}'],
|
|
22
|
+
languageOptions: {
|
|
23
|
+
ecmaVersion: 2022,
|
|
24
|
+
sourceType: 'module',
|
|
25
|
+
globals: { ...globals.browser },
|
|
26
|
+
parserOptions: {
|
|
27
|
+
ecmaFeatures: { jsx: true },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
settings: {
|
|
31
|
+
react: { version: 'detect' },
|
|
32
|
+
},
|
|
33
|
+
plugins: {
|
|
34
|
+
react,
|
|
35
|
+
'react-hooks': reactHooks,
|
|
36
|
+
'react-refresh': reactRefresh,
|
|
37
|
+
},
|
|
38
|
+
rules: {
|
|
39
|
+
...react.configs.flat.recommended.rules,
|
|
40
|
+
...react.configs.flat['jsx-runtime'].rules, // React 18 automatic JSX runtime — no `React` import needed.
|
|
41
|
+
...reactHooks.configs.flat.recommended.rules,
|
|
42
|
+
|
|
43
|
+
// Allow `_`-prefixed intentionally-unused args/vars (shared convention).
|
|
44
|
+
'no-unused-vars': [
|
|
45
|
+
'error',
|
|
46
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
// Fast-refresh: warn (not error) when a module exports non-components
|
|
50
|
+
// alongside components — common + harmless in shadcn ui files.
|
|
51
|
+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
|
52
|
+
|
|
53
|
+
// Project convention: PropTypes are not used (no `prop-types` dep).
|
|
54
|
+
'react/prop-types': 'off',
|
|
55
|
+
|
|
56
|
+
// react-hooks 7.x ships new, very aggressive rules that flag idiomatic
|
|
57
|
+
// patterns (a mount-guard `setState` in an effect, a fetch-on-mount
|
|
58
|
+
// `load()` effect, the lazy-ref-init `if (!ref.current) ref.current = …`
|
|
59
|
+
// one-time init, etc.). These were REVIEWED case-by-case (task #138): every
|
|
60
|
+
// current hit is a known-good idiom, not a bug — promoting them to error
|
|
61
|
+
// would force contorting clean code, so they stay ADVISORY (warn): visible,
|
|
62
|
+
// non-blocking. The genuinely load-bearing hooks rules below stay errors.
|
|
63
|
+
// If a future hit is an actual bug, fix that site; don't relax the warn.
|
|
64
|
+
'react-hooks/set-state-in-effect': 'warn',
|
|
65
|
+
'react-hooks/refs': 'warn',
|
|
66
|
+
// These two catch real bugs — keep them blocking.
|
|
67
|
+
'react-hooks/exhaustive-deps': 'error',
|
|
68
|
+
'react-hooks/rules-of-hooks': 'error',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" suppressHydrationWarning>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
|
+
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
|
+
<title>__APP_NAME__</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "The frontend (UI) half of an Astram consumer app — a professional web UI (login / auth / identity management) over the framework auth API. Built on shadcn/ui + Tailwind v4; shared auth/http logic comes from @astram/frontend-core and @astram/sdk. The API (../server) lives at http://localhost:3000 by default.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"lint": "eslint \"src/**/*.{js,jsx}\"",
|
|
12
|
+
"test": "node --test src/**/*.test.mjs"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@astram/frontend-core": "^0.1.0",
|
|
16
|
+
"@astram/sdk": "^0.2.0",
|
|
17
|
+
"@radix-ui/react-alert-dialog": "^1.1.19",
|
|
18
|
+
"@radix-ui/react-avatar": "^1.1.10",
|
|
19
|
+
"@radix-ui/react-checkbox": "^1.1.4",
|
|
20
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
21
|
+
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
22
|
+
"@radix-ui/react-label": "^2.1.7",
|
|
23
|
+
"@radix-ui/react-select": "^2.3.3",
|
|
24
|
+
"@radix-ui/react-separator": "^1.1.7",
|
|
25
|
+
"@radix-ui/react-slot": "^1.2.3",
|
|
26
|
+
"@radix-ui/react-tooltip": "^1.2.8",
|
|
27
|
+
"class-variance-authority": "^0.7.1",
|
|
28
|
+
"clsx": "^2.1.1",
|
|
29
|
+
"input-otp": "^1.4.2",
|
|
30
|
+
"lucide-react": "^0.460.0",
|
|
31
|
+
"next-themes": "^0.4.6",
|
|
32
|
+
"qrcode": "^1.5.4",
|
|
33
|
+
"react": "^18.3.1",
|
|
34
|
+
"react-dom": "^18.3.1",
|
|
35
|
+
"react-router-dom": "^6.26.0",
|
|
36
|
+
"sonner": "^2.0.7",
|
|
37
|
+
"tailwind-merge": "^3.3.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@eslint/js": "^9.39.5",
|
|
41
|
+
"@tailwindcss/vite": "^4.1.14",
|
|
42
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
43
|
+
"eslint": "^9.39.5",
|
|
44
|
+
"eslint-plugin-react": "^7.37.5",
|
|
45
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
46
|
+
"eslint-plugin-react-refresh": "^0.5.3",
|
|
47
|
+
"globals": "^17.7.0",
|
|
48
|
+
"tailwindcss": "^4.1.14",
|
|
49
|
+
"tw-animate-css": "^1.4.0",
|
|
50
|
+
"vite": "^5.4.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
Binary file
|