@topolo/cli 0.4.9 → 0.5.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/index.js +306 -15
- package/package.json +7 -2
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
|
|
|
9
9
|
import { createInterface } from "readline/promises";
|
|
10
10
|
import { stdin as input } from "process";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// ../topolo-sdk/dist/index.js
|
|
13
13
|
function applyAuthHeaders(headers, credential) {
|
|
14
14
|
if (credential.kind === "api_key") {
|
|
15
15
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -3504,7 +3504,7 @@ function createTopolo(options) {
|
|
|
3504
3504
|
};
|
|
3505
3505
|
}
|
|
3506
3506
|
|
|
3507
|
-
//
|
|
3507
|
+
// ../topolo-sdk/dist/node/credential-store.js
|
|
3508
3508
|
import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
3509
3509
|
import { existsSync } from "fs";
|
|
3510
3510
|
import { homedir, platform } from "os";
|
|
@@ -5819,6 +5819,13 @@ function normalizeWorkspaceSwitcherMode(value) {
|
|
|
5819
5819
|
if (mode === "required" || mode === "optional" || mode === "off") return mode;
|
|
5820
5820
|
throw new Error("Invalid workspace switcher mode. Use required, optional, or off.");
|
|
5821
5821
|
}
|
|
5822
|
+
function normalizeUiTemplate(value) {
|
|
5823
|
+
const template = String(value || "operations").trim();
|
|
5824
|
+
if (template === "operations" || template === "index" || template === "workbench" || template === "minimal") {
|
|
5825
|
+
return template;
|
|
5826
|
+
}
|
|
5827
|
+
throw new Error("Invalid UI template. Use operations, index, workbench, or minimal.");
|
|
5828
|
+
}
|
|
5822
5829
|
function normalizeFirstPartyAppDir(value) {
|
|
5823
5830
|
const requested = String(value).trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
5824
5831
|
const appDir = requested.startsWith("apps/") ? requested.slice("apps/".length) : requested;
|
|
@@ -5927,7 +5934,9 @@ function toPascalCase(value) {
|
|
|
5927
5934
|
return value.split(/[-_]/).filter(Boolean).map((part) => acronyms.has(part.toLowerCase()) ? part.toUpperCase() : part.slice(0, 1).toUpperCase() + part.slice(1)).join("");
|
|
5928
5935
|
}
|
|
5929
5936
|
function toTitle(value) {
|
|
5930
|
-
|
|
5937
|
+
const withoutVendorPrefix = value.replace(/^topolo[-_\s]+/i, "");
|
|
5938
|
+
const source = withoutVendorPrefix.trim() ? withoutVendorPrefix : value;
|
|
5939
|
+
return source.split(/[-_\s]+/).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
5931
5940
|
}
|
|
5932
5941
|
function json(value) {
|
|
5933
5942
|
return JSON.stringify(value, null, 2);
|
|
@@ -6033,6 +6042,9 @@ function packageJson(application, target, options) {
|
|
|
6033
6042
|
devDependencies["@types/react-dom"] = "^19.2.2";
|
|
6034
6043
|
devDependencies["@vitejs/plugin-react"] = "^4.7.0";
|
|
6035
6044
|
if (target === "platform") {
|
|
6045
|
+
devDependencies.autoprefixer = "10.5.0";
|
|
6046
|
+
devDependencies.postcss = "8.5.6";
|
|
6047
|
+
devDependencies.tailwindcss = "3.4.19";
|
|
6036
6048
|
for (const packageName of PLATFORM_BROWSER_TOPOLO_PACKAGES) {
|
|
6037
6049
|
dependencies[packageName] = topoloPackageVersion(packageName, target);
|
|
6038
6050
|
}
|
|
@@ -6196,13 +6208,16 @@ function readme(application, target, options) {
|
|
|
6196
6208
|
"- `@topolo-io/app-funnel` provides the generated public application funnel/landing surface.",
|
|
6197
6209
|
"- `@topolo-io/app-shell` provides the generated route/auth bootstrap, product shell, login, callback, and loading surfaces.",
|
|
6198
6210
|
"- `@topolo-io/app-launcher` provides shared app switching and handoff helpers.",
|
|
6199
|
-
"- `@topolo-io/ui-kit` provides
|
|
6211
|
+
"- `@topolo-io/ui-kit` provides every control, table, panel, workbench, inspector, icon, transition, and bug reporter primitive used by the generated product surface.",
|
|
6200
6212
|
"- `@topolo-io/auth-client` owns browser auth, callback exchange, token refresh, and logout.",
|
|
6201
6213
|
"- `@topolo-io/api-key-management`, `@topolo-io/chat-widget`, `@topolo-io/notifications`, and `@topolo-io/resource-context` complete the AppShell capability baseline.",
|
|
6202
6214
|
"- Every Topolo-owned browser dependency is emitted at `latest`; product-domain packages are added only when the app actually adopts that domain.",
|
|
6203
6215
|
"- `.npmrc` routes private `@topolo-io` packages to GitHub Packages without storing credentials; public `@topolo` packages remain on npm. Local and CI environments must provide a GitHub Packages token.",
|
|
6204
6216
|
"- Generated first-party browser apps start on the shared landing page at `/`, use the shared login page at `/login`, and complete sign-in on `/auth/callback` before entering the configured authenticated home.",
|
|
6205
6217
|
"- The workspace selector renders in the top utility bar; Getting Started is an account-menu action and is not part of the left product rail.",
|
|
6218
|
+
"- The default `operations` UI template separates dashboard, CRUD index, detail workbench, and inspector concerns. Use `--ui-template index`, `--ui-template workbench`, or `--ui-template minimal` only when that product shape is intentional.",
|
|
6219
|
+
"- Every visible product navigation item includes a canonical icon so the shared sidebar remains usable when collapsed.",
|
|
6220
|
+
"- `styles.css` contains only Tailwind directives; app-local controls, semantic tables, or replacement design-system CSS fail the generated UI contract test.",
|
|
6206
6221
|
"- `vite.config.ts` includes the standard React plugin plus React runtime dedupe so linked Topolo packages resolve one browser React runtime."
|
|
6207
6222
|
);
|
|
6208
6223
|
if (options.api) {
|
|
@@ -6299,6 +6314,8 @@ function agentRules(application, target, options) {
|
|
|
6299
6314
|
"- Start from `staging`, preserve other agents' work, and do not revert unrelated local changes.",
|
|
6300
6315
|
"- Make contained changes at the real ownership boundary; do not patch around shared runtime, auth, launcher, shell, or resource-context issues locally.",
|
|
6301
6316
|
"- Browser code should stay on the shared landing/login/callback/AppShell baseline generated by the scaffold.",
|
|
6317
|
+
"- Keep every visible product navigation item on a canonical `@topolo-io/ui-kit/icons` icon so collapsed navigation remains complete.",
|
|
6318
|
+
"- Use shared UI-kit controls, tables, panels, workbenches, and inspectors; the generated UI contract intentionally rejects app-local native controls and tables.",
|
|
6302
6319
|
...options.api ? ["- API code should stay on `@topolo-io/worker-runtime`, derive auth context from verified credentials, and preserve Nexus usage monitoring."] : [],
|
|
6303
6320
|
...hasScaffoldResources(options.resources) ? ["- Keep Cloudflare resource bindings in Wrangler, CloudControl, Env typing, docs, and README verification output in sync."] : [],
|
|
6304
6321
|
"",
|
|
@@ -6649,7 +6666,36 @@ function mainTsx() {
|
|
|
6649
6666
|
""
|
|
6650
6667
|
].join("\n");
|
|
6651
6668
|
}
|
|
6652
|
-
function
|
|
6669
|
+
function tailwindConfigCjs() {
|
|
6670
|
+
return [
|
|
6671
|
+
"const { createTopoloTailwindConfig } = require('@topolo-io/design-tokens/tailwind.config.cjs');",
|
|
6672
|
+
"",
|
|
6673
|
+
"module.exports = createTopoloTailwindConfig({",
|
|
6674
|
+
" content: [",
|
|
6675
|
+
" './apps/web/index.html',",
|
|
6676
|
+
" './apps/web/src/**/*.{js,jsx,ts,tsx}',",
|
|
6677
|
+
" ],",
|
|
6678
|
+
"});",
|
|
6679
|
+
""
|
|
6680
|
+
].join("\n");
|
|
6681
|
+
}
|
|
6682
|
+
function postcssConfigCjs() {
|
|
6683
|
+
return [
|
|
6684
|
+
"const path = require('node:path');",
|
|
6685
|
+
"",
|
|
6686
|
+
"module.exports = {",
|
|
6687
|
+
" plugins: {",
|
|
6688
|
+
" tailwindcss: { config: path.join(__dirname, 'tailwind.config.cjs') },",
|
|
6689
|
+
" autoprefixer: {},",
|
|
6690
|
+
" },",
|
|
6691
|
+
"};",
|
|
6692
|
+
""
|
|
6693
|
+
].join("\n");
|
|
6694
|
+
}
|
|
6695
|
+
function stylesCss(target) {
|
|
6696
|
+
if (target === "platform") {
|
|
6697
|
+
return ["@tailwind base;", "@tailwind components;", "@tailwind utilities;", ""].join("\n");
|
|
6698
|
+
}
|
|
6653
6699
|
return [
|
|
6654
6700
|
"/* Local app styles. Design tokens come from @topolo-io/design-tokens (imported in main.tsx). */",
|
|
6655
6701
|
"/* Use hsl(var(--token)) for colors so light/dark themes flip from the shared shell. */",
|
|
@@ -6855,20 +6901,41 @@ export const APP_DISPLAY_NAME = '${escapeSingleQuoted(appName)}';
|
|
|
6855
6901
|
`;
|
|
6856
6902
|
}
|
|
6857
6903
|
function platformAppRoutesTsx(experience) {
|
|
6858
|
-
const nav = experience.navigationMode === "none" ? "" : " nav: { id: 'home', label: HOME_LABEL },\n";
|
|
6904
|
+
const nav = experience.navigationMode === "none" ? "" : " nav: { id: 'home', label: HOME_LABEL, icon: LayoutDashboard },\n";
|
|
6905
|
+
const recordsNav = experience.navigationMode === "none" ? "" : " nav: { id: 'records', label: 'Records', icon: ListTree },\n";
|
|
6906
|
+
const starterImports = experience.uiTemplate === "minimal" ? "" : "import RecordDetailRoute from './routes/RecordDetailRoute';\nimport RecordsRoute from './routes/RecordsRoute';\n";
|
|
6907
|
+
const homeElement = experience.uiTemplate === "index" ? "<RecordsRoute />" : experience.uiTemplate === "workbench" ? "<RecordDetailRoute />" : "<HomeRoute />";
|
|
6908
|
+
const starterRoutes = experience.uiTemplate === "minimal" ? "" : experience.uiTemplate === "operations" ? ` {
|
|
6909
|
+
path: '/records',
|
|
6910
|
+
${recordsNav} loading: { shell: NAVIGATION_MODE, content: 'table', title: 'Records' },
|
|
6911
|
+
element: <RecordsRoute />,
|
|
6912
|
+
},
|
|
6913
|
+
{
|
|
6914
|
+
path: '/records/:recordId/:section',
|
|
6915
|
+
loading: { shell: NAVIGATION_MODE, content: 'split', title: 'Record' },
|
|
6916
|
+
element: <RecordDetailRoute />,
|
|
6917
|
+
},
|
|
6918
|
+
` : ` {
|
|
6919
|
+
path: '/records/:recordId/:section',
|
|
6920
|
+
loading: { shell: NAVIGATION_MODE, content: 'split', title: 'Record' },
|
|
6921
|
+
element: <RecordDetailRoute />,
|
|
6922
|
+
},
|
|
6923
|
+
`;
|
|
6859
6924
|
return `import type { AppRoute } from '@topolo-io/app-shell';
|
|
6925
|
+
import { LayoutDashboard, ListTree } from '@topolo-io/ui-kit/icons';
|
|
6860
6926
|
import { HOME_LABEL, HOME_PATH, NAVIGATION_MODE } from './app-config';
|
|
6861
6927
|
import GettingStartedRoute from './routes/GettingStartedRoute';
|
|
6862
6928
|
import HomeRoute from './routes/HomeRoute';
|
|
6929
|
+
${starterImports}
|
|
6863
6930
|
|
|
6864
6931
|
export function createAppRoutes(appId: string): AppRoute[] {
|
|
6865
6932
|
return [
|
|
6866
6933
|
{
|
|
6867
6934
|
path: HOME_PATH,
|
|
6868
6935
|
${nav} loading: { shell: NAVIGATION_MODE, content: 'workspace', title: HOME_LABEL },
|
|
6869
|
-
element:
|
|
6936
|
+
element: ${homeElement},
|
|
6870
6937
|
},
|
|
6871
|
-
{
|
|
6938
|
+
${starterRoutes} {
|
|
6872
6939
|
path: '/getting-started',
|
|
6873
6940
|
loading: { shell: NAVIGATION_MODE, content: 'card-grid', title: 'Getting started' },
|
|
6874
6941
|
element: <GettingStartedRoute appId={appId} />,
|
|
@@ -6877,7 +6944,8 @@ ${nav} loading: { shell: NAVIGATION_MODE, content: 'workspace', title: HOME
|
|
|
6877
6944
|
}
|
|
6878
6945
|
`;
|
|
6879
6946
|
}
|
|
6880
|
-
function platformHomeRouteTsx() {
|
|
6947
|
+
function platformHomeRouteTsx(uiTemplate) {
|
|
6948
|
+
if (uiTemplate === "operations") return platformOperationsHomeRouteTsx();
|
|
6881
6949
|
return `import { TopoloAppPage, TopoloAppPageHeader, useAuth } from '@topolo-io/app-shell';
|
|
6882
6950
|
import { IntroTour } from '@topolo-io/tour';
|
|
6883
6951
|
import { useTranslation } from '@topolo-io/ui-kit';
|
|
@@ -6913,6 +6981,148 @@ export default function HomeRoute() {
|
|
|
6913
6981
|
}
|
|
6914
6982
|
`;
|
|
6915
6983
|
}
|
|
6984
|
+
function platformOperationsHomeRouteTsx() {
|
|
6985
|
+
return `import { TopoloAppPage, TopoloAppPageHeader, useAuth } from '@topolo-io/app-shell';
|
|
6986
|
+
import { Button, ListRow, TopoloMetricStrip, TopoloOperationalPanel } from '@topolo-io/ui-kit';
|
|
6987
|
+
import { IntroTour } from '@topolo-io/tour';
|
|
6988
|
+
import { useNavigate } from 'react-router-dom';
|
|
6989
|
+
import { APP_NAME, HOME_LABEL, HOME_PATH, PRODUCT_TAGLINE } from '../app-config';
|
|
6990
|
+
import { STARTER_RECORDS } from './starter-records';
|
|
6991
|
+
|
|
6992
|
+
// STARTER TEMPLATE: replace the sample metrics and activity with product data.
|
|
6993
|
+
export default function HomeRoute() {
|
|
6994
|
+
const { user } = useAuth();
|
|
6995
|
+
const navigate = useNavigate();
|
|
6996
|
+
const active = STARTER_RECORDS.filter((record) => record.status === 'active').length;
|
|
6997
|
+
return (
|
|
6998
|
+
<TopoloAppPage width="none" data-tour="home-page">
|
|
6999
|
+
<TopoloAppPageHeader eyebrow={APP_NAME} title={HOME_LABEL} description={PRODUCT_TAGLINE} actions={<Button onClick={() => navigate('/records')}>Open records</Button>} />
|
|
7000
|
+
<TopoloMetricStrip metrics={[
|
|
7001
|
+
{ id: 'total', label: 'Total records', value: STARTER_RECORDS.length },
|
|
7002
|
+
{ id: 'active', label: 'Active', value: active, tone: 'success' },
|
|
7003
|
+
{ id: 'attention', label: 'Needs attention', value: STARTER_RECORDS.length - active, tone: 'warning' },
|
|
7004
|
+
]} />
|
|
7005
|
+
<TopoloOperationalPanel title="Recent records" description="Keep operational summaries focused; detailed CRUD belongs in the index and workbench routes." action={<Button variant="secondary" onClick={() => navigate('/records')}>View index</Button>}>
|
|
7006
|
+
<div className="grid gap-1">{STARTER_RECORDS.slice(0, 3).map((record) => <ListRow key={record.id} title={record.name} subtitle={record.owner} meta={record.updatedAt} onSelect={() => navigate(\`/records/\${record.id}/overview\`)} />)}</div>
|
|
7007
|
+
</TopoloOperationalPanel>
|
|
7008
|
+
<IntroTour appName={APP_NAME} role={user?.role} onOpenGettingStarted={() => navigate('/getting-started')} extraSteps={[{
|
|
7009
|
+
id: 'home-page', target: '[data-tour="home-page"]', title: HOME_LABEL, content: PRODUCT_TAGLINE,
|
|
7010
|
+
placement: 'bottom' as const, beforeStep: () => navigate(HOME_PATH),
|
|
7011
|
+
}]} />
|
|
7012
|
+
</TopoloAppPage>
|
|
7013
|
+
);
|
|
7014
|
+
}
|
|
7015
|
+
`;
|
|
7016
|
+
}
|
|
7017
|
+
function platformStarterDataTs() {
|
|
7018
|
+
return `// STARTER TEMPLATE: replace this file with the app's real API-backed domain model.
|
|
7019
|
+
export type StarterRecordStatus = 'active' | 'draft' | 'paused';
|
|
7020
|
+
|
|
7021
|
+
export interface StarterRecord {
|
|
7022
|
+
id: string;
|
|
7023
|
+
name: string;
|
|
7024
|
+
status: StarterRecordStatus;
|
|
7025
|
+
owner: string;
|
|
7026
|
+
summary: string;
|
|
7027
|
+
updatedAt: string;
|
|
7028
|
+
}
|
|
7029
|
+
|
|
7030
|
+
export const STARTER_RECORDS: StarterRecord[] = [
|
|
7031
|
+
{ id: 'starter-record-1', name: 'First starter record', status: 'active', owner: 'Product team', summary: 'Replace this sample with one app-specific concept.', updatedAt: 'Today' },
|
|
7032
|
+
{ id: 'starter-record-2', name: 'Second starter record', status: 'draft', owner: 'Operations', summary: 'Use the index for discovery and the workbench for focused editing.', updatedAt: 'Yesterday' },
|
|
7033
|
+
{ id: 'starter-record-3', name: 'Third starter record', status: 'paused', owner: 'Workspace admin', summary: 'Keep unrelated workflows on separate routes.', updatedAt: '3 days ago' },
|
|
7034
|
+
];
|
|
7035
|
+
`;
|
|
7036
|
+
}
|
|
7037
|
+
function platformRecordsRouteTsx() {
|
|
7038
|
+
return `import { useMemo, useState, type FormEvent } from 'react';
|
|
7039
|
+
import { TopoloAppDataTable, TopoloAppEmptyState, TopoloAppFilterBar, TopoloAppPage, TopoloAppPageHeader, TopoloAppToolbar } from '@topolo-io/app-shell';
|
|
7040
|
+
import { Badge, Button, FormField, Input, SearchInput, Select, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, TopoloRightPanel } from '@topolo-io/ui-kit';
|
|
7041
|
+
import { Plus } from '@topolo-io/ui-kit/icons';
|
|
7042
|
+
import { useNavigate } from 'react-router-dom';
|
|
7043
|
+
import { STARTER_RECORDS, type StarterRecord, type StarterRecordStatus } from './starter-records';
|
|
7044
|
+
|
|
7045
|
+
// STARTER TEMPLATE: keep the index focused on discovery, filtering, and create entry.
|
|
7046
|
+
export default function RecordsRoute() {
|
|
7047
|
+
const navigate = useNavigate();
|
|
7048
|
+
const [records, setRecords] = useState<StarterRecord[]>(STARTER_RECORDS);
|
|
7049
|
+
const [query, setQuery] = useState('');
|
|
7050
|
+
const [status, setStatus] = useState('current');
|
|
7051
|
+
const [createOpen, setCreateOpen] = useState(false);
|
|
7052
|
+
const visible = useMemo(() => records.filter((record) => {
|
|
7053
|
+
if (status !== 'current' && status !== 'all' && record.status !== status) return false;
|
|
7054
|
+
return !query || record.name.toLowerCase().includes(query.toLowerCase());
|
|
7055
|
+
}), [query, records, status]);
|
|
7056
|
+
|
|
7057
|
+
function createRecord(event: FormEvent<HTMLFormElement>) {
|
|
7058
|
+
event.preventDefault();
|
|
7059
|
+
const data = new FormData(event.currentTarget);
|
|
7060
|
+
const name = String(data.get('name') || '').trim();
|
|
7061
|
+
if (!name) return;
|
|
7062
|
+
const record: StarterRecord = {
|
|
7063
|
+
id: \`starter-record-\${Date.now()}\`, name, status: 'draft', owner: String(data.get('owner') || 'Workspace team'),
|
|
7064
|
+
summary: String(data.get('summary') || 'Replace this starter form with domain fields.'), updatedAt: 'Just now',
|
|
7065
|
+
};
|
|
7066
|
+
setRecords((current) => [record, ...current]);
|
|
7067
|
+
setCreateOpen(false);
|
|
7068
|
+
}
|
|
7069
|
+
|
|
7070
|
+
return (
|
|
7071
|
+
<TopoloAppPage width="none">
|
|
7072
|
+
<TopoloAppPageHeader eyebrow="Starter CRUD" title="Records" description="A canonical index route with shared controls, shared tables, and a separate creation panel." actions={<Button leftIcon={<Plus size={16} />} onClick={() => setCreateOpen(true)}>New record</Button>} />
|
|
7073
|
+
<TopoloAppToolbar><TopoloAppFilterBar>
|
|
7074
|
+
<SearchInput className="w-64" value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search records" aria-label="Search records" />
|
|
7075
|
+
<Select className="w-44" value={status} onChange={(event) => setStatus(event.target.value)} aria-label="Record status"><option value="current">Current</option><option value="active">Active</option><option value="draft">Draft</option><option value="paused">Paused</option><option value="all">All</option></Select>
|
|
7076
|
+
</TopoloAppFilterBar><span className="text-sm text-muted-foreground">{visible.length} result{visible.length === 1 ? '' : 's'}</span></TopoloAppToolbar>
|
|
7077
|
+
{visible.length ? <TopoloAppDataTable aria-label="Records"><Table>
|
|
7078
|
+
<TableHeader><TableRow><TableHead>Name</TableHead><TableHead>Status</TableHead><TableHead>Owner</TableHead><TableHead>Updated</TableHead></TableRow></TableHeader>
|
|
7079
|
+
<TableBody>{visible.map((record) => <TableRow className="cursor-pointer" key={record.id} tabIndex={0} onClick={() => navigate(\`/records/\${record.id}/overview\`)} onKeyDown={(event) => { if (event.key === 'Enter') navigate(\`/records/\${record.id}/overview\`); }}>
|
|
7080
|
+
<TableCell><span className="grid gap-0.5"><strong>{record.name}</strong><small className="text-xs text-muted-foreground">{record.id}</small></span></TableCell>
|
|
7081
|
+
<TableCell><RecordStatus status={record.status} /></TableCell><TableCell>{record.owner}</TableCell><TableCell>{record.updatedAt}</TableCell>
|
|
7082
|
+
</TableRow>)}</TableBody>
|
|
7083
|
+
</Table></TopoloAppDataTable> : <TopoloAppEmptyState title="No records match" description="Adjust the filters or create a record." action={<Button onClick={() => setCreateOpen(true)}>Create record</Button>} />}
|
|
7084
|
+
<TopoloRightPanel open={createOpen} onOpenChange={setCreateOpen} title="Create record" description="Replace these starter fields with the smallest valid domain create flow." footer={<Button type="submit" form="starter-create-record">Create record</Button>}>
|
|
7085
|
+
<form id="starter-create-record" className="grid gap-5" onSubmit={createRecord}><FormField label="Name"><Input name="name" required autoFocus /></FormField><FormField label="Owner"><Input name="owner" placeholder="Workspace team" /></FormField><FormField label="Summary"><Input name="summary" placeholder="What this record represents" /></FormField></form>
|
|
7086
|
+
</TopoloRightPanel>
|
|
7087
|
+
</TopoloAppPage>
|
|
7088
|
+
);
|
|
7089
|
+
}
|
|
7090
|
+
|
|
7091
|
+
export function RecordStatus({ status }: { status: StarterRecordStatus }) {
|
|
7092
|
+
const tone = status === 'active' ? 'success' : status === 'paused' ? 'warning' : 'info';
|
|
7093
|
+
return <Badge tone={tone}>{status}</Badge>;
|
|
7094
|
+
}
|
|
7095
|
+
`;
|
|
7096
|
+
}
|
|
7097
|
+
function platformRecordDetailRouteTsx() {
|
|
7098
|
+
return `import { TopoloAppEmptyState } from '@topolo-io/app-shell';
|
|
7099
|
+
import { Button, CopyField, Tabs, TabsList, TabsTrigger, TopoloCommandBar, TopoloInspectorPanel, TopoloKeyValueRow, TopoloOperationalPanel, TopoloWorkbench } from '@topolo-io/ui-kit';
|
|
7100
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
7101
|
+
import { RecordStatus } from './RecordsRoute';
|
|
7102
|
+
import { STARTER_RECORDS } from './starter-records';
|
|
7103
|
+
|
|
7104
|
+
const sections = ['overview', 'activity', 'settings'] as const;
|
|
7105
|
+
|
|
7106
|
+
// STARTER TEMPLATE: one selected record, one focused workflow, one inspector. The product sidebar remains the only left rail.
|
|
7107
|
+
export default function RecordDetailRoute() {
|
|
7108
|
+
const navigate = useNavigate();
|
|
7109
|
+
const { recordId, section: rawSection } = useParams();
|
|
7110
|
+
const record = STARTER_RECORDS.find((item) => item.id === recordId) ?? STARTER_RECORDS[0];
|
|
7111
|
+
const section = sections.includes(rawSection as (typeof sections)[number]) ? rawSection! : 'overview';
|
|
7112
|
+
if (!record) return <TopoloAppEmptyState title="Record not found" description="Return to the index and choose another record." action={<Button onClick={() => navigate('/records')}>Back to records</Button>} />;
|
|
7113
|
+
return (
|
|
7114
|
+
<div className="-m-6 min-w-0 max-md:-m-4"><TopoloWorkbench layout="split" className="min-h-[calc(100dvh-4rem)] gap-0 overflow-hidden bg-background">
|
|
7115
|
+
<main className="min-w-0 overflow-y-auto p-6 max-md:p-4">
|
|
7116
|
+
<TopoloCommandBar eyebrow="Starter record" title={record.name} description={record.summary} meta={<RecordStatus status={record.status} />} actions={<Button>Edit record</Button>} />
|
|
7117
|
+
<Tabs className="my-5" value={section} onValueChange={(next) => navigate(\`/records/\${record.id}/\${next}\`)}><TabsList>{sections.map((item) => <TabsTrigger key={item} value={item}>{item}</TabsTrigger>)}</TabsList></Tabs>
|
|
7118
|
+
<TopoloOperationalPanel title={section === 'overview' ? 'Record overview' : section === 'activity' ? 'Record activity' : 'Record settings'} description="Replace this panel with one focused domain workflow. Do not combine unrelated concepts on this route."><TopoloKeyValueRow label="Owner" value={record.owner} /><TopoloKeyValueRow label="Updated" value={record.updatedAt} /></TopoloOperationalPanel>
|
|
7119
|
+
</main>
|
|
7120
|
+
<TopoloInspectorPanel className="m-4 ml-0 max-lg:m-4" title="Record context" description="Keep selected-record metadata and secondary actions here." sticky><TopoloKeyValueRow label="Status" value={<RecordStatus status={record.status} />} /><TopoloKeyValueRow label="Owner" value={record.owner} /><CopyField label="Record ID" value={record.id} /></TopoloInspectorPanel>
|
|
7121
|
+
</TopoloWorkbench></div>
|
|
7122
|
+
);
|
|
7123
|
+
}
|
|
7124
|
+
`;
|
|
7125
|
+
}
|
|
6916
7126
|
function platformGettingStartedRouteTsx() {
|
|
6917
7127
|
return `import { TopoloAppPage, useAuth } from '@topolo-io/app-shell';
|
|
6918
7128
|
import { GettingStarted, type OnboardingGuideStep } from '@topolo-io/onboarding';
|
|
@@ -7866,7 +8076,7 @@ function topoloConformanceWorkflow(options) {
|
|
|
7866
8076
|
" - name: Install generated application",
|
|
7867
8077
|
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
7868
8078
|
` working-directory: ${appPath}`,
|
|
7869
|
-
" run: pnpm install --frozen-lockfile",
|
|
8079
|
+
" run: pnpm install --no-frozen-lockfile",
|
|
7870
8080
|
"",
|
|
7871
8081
|
" - name: Run generated application quality gates",
|
|
7872
8082
|
` if: steps.app-token.outputs.token != '' || env.HAS_TOPOLO_CI_REPO_TOKEN == 'true'`,
|
|
@@ -8804,7 +9014,8 @@ ${coverageThresholds}
|
|
|
8804
9014
|
function platformBrowserContractTest(application, experience) {
|
|
8805
9015
|
const required = experience.workspaceSwitcher === "required";
|
|
8806
9016
|
const enabled = experience.workspaceSwitcher !== "off";
|
|
8807
|
-
const navExpectation = experience.navigationMode === "none" ? " expect(home.nav).toBeUndefined();" : ` expect(home.nav).
|
|
9017
|
+
const navExpectation = experience.navigationMode === "none" ? " expect(home.nav).toBeUndefined();" : ` expect(home.nav).toMatchObject({ id: 'home', label: '${escapeSingleQuoted(experience.homeLabel)}' });
|
|
9018
|
+
expect(home.nav?.icon).toBeDefined();`;
|
|
8808
9019
|
return `import { describe, expect, it, vi } from 'vitest';
|
|
8809
9020
|
|
|
8810
9021
|
vi.mock('../src/app-identity', () => ({
|
|
@@ -8825,12 +9036,52 @@ describe('generated application contract', () => {
|
|
|
8825
9036
|
});
|
|
8826
9037
|
|
|
8827
9038
|
it('keeps Getting Started out of product navigation', () => {
|
|
8828
|
-
const
|
|
9039
|
+
const routes = createAppRoutes('app_test');
|
|
9040
|
+
const home = routes.find((route) => route.path === '${escapeSingleQuoted(experience.homePath)}')!;
|
|
9041
|
+
const gettingStarted = routes.find((route) => route.path === '/getting-started')!;
|
|
8829
9042
|
expect(home.path).toBe('${escapeSingleQuoted(experience.homePath)}');
|
|
8830
9043
|
${navExpectation}
|
|
8831
9044
|
expect(gettingStarted.path).toBe('/getting-started');
|
|
8832
9045
|
expect(gettingStarted.nav).toBeUndefined();
|
|
8833
9046
|
});
|
|
9047
|
+
|
|
9048
|
+
it('keeps every visible route reachable when a sidebar is collapsed', () => {
|
|
9049
|
+
for (const route of createAppRoutes('app_test').filter((candidate) => candidate.nav)) {
|
|
9050
|
+
expect(route.nav?.icon).toBeDefined();
|
|
9051
|
+
}
|
|
9052
|
+
});
|
|
9053
|
+
});
|
|
9054
|
+
`;
|
|
9055
|
+
}
|
|
9056
|
+
function platformUiKitContractTest() {
|
|
9057
|
+
return `import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
9058
|
+
import { join } from 'node:path';
|
|
9059
|
+
import { describe, expect, it } from 'vitest';
|
|
9060
|
+
|
|
9061
|
+
function sourceFiles(root: string): string[] {
|
|
9062
|
+
return readdirSync(root).flatMap((entry) => {
|
|
9063
|
+
const path = join(root, entry);
|
|
9064
|
+
return statSync(path).isDirectory() ? sourceFiles(path) : path.endsWith('.tsx') ? [path] : [];
|
|
9065
|
+
});
|
|
9066
|
+
}
|
|
9067
|
+
|
|
9068
|
+
describe('generated shared UI contract', () => {
|
|
9069
|
+
const sourceRoot = join(process.cwd(), 'src');
|
|
9070
|
+
const source = sourceFiles(sourceRoot).map((path) => readFileSync(path, 'utf8')).join('\\n');
|
|
9071
|
+
|
|
9072
|
+
it('does not hand-roll controls or semantic tables in first-party routes', () => {
|
|
9073
|
+
expect(source).not.toMatch(/<(button|input|select|textarea|table|thead|tbody|tr|th|td)(\\s|>)/);
|
|
9074
|
+
});
|
|
9075
|
+
|
|
9076
|
+
it('keeps app-local CSS limited to the shared Tailwind pipeline', () => {
|
|
9077
|
+
expect(readFileSync(join(sourceRoot, 'styles.css'), 'utf8').trim()).toBe('@tailwind base;\\n@tailwind components;\\n@tailwind utilities;');
|
|
9078
|
+
});
|
|
9079
|
+
|
|
9080
|
+
it('scans app source and shared packages through the canonical token config', () => {
|
|
9081
|
+
const config = readFileSync(join(process.cwd(), 'tailwind.config.cjs'), 'utf8');
|
|
9082
|
+
expect(config).toContain('createTopoloTailwindConfig');
|
|
9083
|
+
expect(config).toContain("./apps/web/src/**/*.{js,jsx,ts,tsx}");
|
|
9084
|
+
});
|
|
8834
9085
|
});
|
|
8835
9086
|
`;
|
|
8836
9087
|
}
|
|
@@ -8988,6 +9239,7 @@ function applicationScaffoldPayload(application, options = {}) {
|
|
|
8988
9239
|
homeLabel: normalizeHomeLabel(options.homeLabel),
|
|
8989
9240
|
navigationMode: normalizeNavigationMode(options.navigationMode),
|
|
8990
9241
|
workspaceSwitcher: normalizeWorkspaceSwitcherMode(options.workspaceSwitcher),
|
|
9242
|
+
uiTemplate: normalizeUiTemplate(options.uiTemplate),
|
|
8991
9243
|
workspaceLabel: tenancy === "household" ? "Households" : "Workspaces",
|
|
8992
9244
|
tagline: String(options.tagline || "").trim() || `Manage your work in ${toTitle(name)}.`
|
|
8993
9245
|
};
|
|
@@ -9267,9 +9519,21 @@ function scaffoldFiles(options) {
|
|
|
9267
9519
|
"does not fork the shared shell chrome",
|
|
9268
9520
|
"keeps app-specific layout scoped under generated classes"
|
|
9269
9521
|
],
|
|
9270
|
-
content: stylesCss()
|
|
9522
|
+
content: stylesCss(target)
|
|
9271
9523
|
}] : [],
|
|
9272
9524
|
...target === "platform" && options.browser ? [
|
|
9525
|
+
{
|
|
9526
|
+
path: `${browserBase}/tailwind.config.cjs`,
|
|
9527
|
+
purpose: "Canonical shared-token Tailwind content configuration.",
|
|
9528
|
+
requiredSignals: ["extends the shared Topolo token config", "scans app source correctly when Vite starts from the repository root"],
|
|
9529
|
+
content: tailwindConfigCjs()
|
|
9530
|
+
},
|
|
9531
|
+
{
|
|
9532
|
+
path: `${browserBase}/postcss.config.cjs`,
|
|
9533
|
+
purpose: "PostCSS pipeline for shared Topolo utility classes.",
|
|
9534
|
+
requiredSignals: ["loads the generated Tailwind config", "runs autoprefixer"],
|
|
9535
|
+
content: postcssConfigCjs()
|
|
9536
|
+
},
|
|
9273
9537
|
{
|
|
9274
9538
|
path: `${browserBase}/src/app-config.ts`,
|
|
9275
9539
|
purpose: "Product route, navigation, context-switcher, and entry-surface configuration.",
|
|
@@ -9286,8 +9550,28 @@ function scaffoldFiles(options) {
|
|
|
9286
9550
|
path: `${browserBase}/src/routes/HomeRoute.tsx`,
|
|
9287
9551
|
purpose: "Focused authenticated home product surface.",
|
|
9288
9552
|
requiredSignals: ["uses shared page primitives", "uses translation-safe product copy"],
|
|
9289
|
-
content: platformHomeRouteTsx()
|
|
9553
|
+
content: platformHomeRouteTsx(options.payload.experience.uiTemplate)
|
|
9290
9554
|
},
|
|
9555
|
+
...options.payload.experience.uiTemplate === "minimal" ? [] : [
|
|
9556
|
+
{
|
|
9557
|
+
path: `${browserBase}/src/routes/starter-records.ts`,
|
|
9558
|
+
purpose: "Clearly marked replaceable starter records for the generated UI template.",
|
|
9559
|
+
requiredSignals: ["keeps sample data outside route components", "is explicitly marked for replacement"],
|
|
9560
|
+
content: platformStarterDataTs()
|
|
9561
|
+
},
|
|
9562
|
+
{
|
|
9563
|
+
path: `${browserBase}/src/routes/RecordsRoute.tsx`,
|
|
9564
|
+
purpose: "Focused CRUD index template built exclusively from shared UI primitives.",
|
|
9565
|
+
requiredSignals: ["uses shared table, search, select, button, and right-panel primitives", "keeps creation separate from the index"],
|
|
9566
|
+
content: platformRecordsRouteTsx()
|
|
9567
|
+
},
|
|
9568
|
+
{
|
|
9569
|
+
path: `${browserBase}/src/routes/RecordDetailRoute.tsx`,
|
|
9570
|
+
purpose: "Focused record workbench template with collection rail and inspector.",
|
|
9571
|
+
requiredSignals: ["uses the shared workbench and inspector primitives", "keeps detail sections URL-addressable"],
|
|
9572
|
+
content: platformRecordDetailRouteTsx()
|
|
9573
|
+
}
|
|
9574
|
+
],
|
|
9291
9575
|
{
|
|
9292
9576
|
path: `${browserBase}/src/routes/GettingStartedRoute.tsx`,
|
|
9293
9577
|
purpose: "Focused role-aware onboarding route reached from the account menu.",
|
|
@@ -9570,6 +9854,12 @@ export default defineConfig({
|
|
|
9570
9854
|
requiredSignals: ["proves API proxying", "proves non-API browser fallback"],
|
|
9571
9855
|
content: platformWebWorkerTest(application, options.api)
|
|
9572
9856
|
},
|
|
9857
|
+
{
|
|
9858
|
+
path: `${browserBase}/test/ui-kit-contract.test.ts`,
|
|
9859
|
+
purpose: "Executable guard against hand-rolled first-party controls and broken shared styling.",
|
|
9860
|
+
requiredSignals: ["rejects native controls and semantic tables in generated routes", "proves canonical Tailwind scanning and zero app-local design CSS"],
|
|
9861
|
+
content: platformUiKitContractTest()
|
|
9862
|
+
},
|
|
9573
9863
|
{
|
|
9574
9864
|
path: `${browserBase}/src/worker.ts`,
|
|
9575
9865
|
purpose: "Browser Worker that serves assets and forwards API paths when an API target exists.",
|
|
@@ -10420,7 +10710,7 @@ function registerPlatformCommand(program2) {
|
|
|
10420
10710
|
).option("--json", "Output JSON").option("--no-json", "Output human-readable checklist").option("--app-dir <path>", "Application repo name or apps/<repo> path").option("--name <name>", "Application display/package source name").option("--package-name <name>", "package.json name").option("--production-url <url>", "Production browser URL").option("--service <id>", "Stable SDK/Auth app id for API-backed apps").option("--portfolio <portfolio>", "First-party portfolio: platform or personal").option(
|
|
10421
10711
|
"--surface-model <surfaceModel>",
|
|
10422
10712
|
"First-party surface model: web_app, mobile_container, mobile_module, or messaging_surface"
|
|
10423
|
-
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--with-d1", "Declare a D1 binding in the first-party scaffold").option("--with-r2", "Declare an R2 binding in the first-party scaffold").option("--with-queue", "Declare a Queue binding in the first-party scaffold").option("--with-workflow", "Declare a Workflow binding in the first-party scaffold").option("--with-container-processing", "Declare container-processing metadata in CloudControl").option("--home-path <path>", "Authenticated home route (default: /dashboard)").option("--home-label <label>", "Home navigation label (default: Dashboard)").option("--navigation-mode <mode>", "Product navigation: sidebar, topbar, or none").option("--workspace-switcher <mode>", "Workspace context selector: required, optional, or off").option("--tagline <text>", "Product-facing landing and home description").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
10713
|
+
).option("--container-app-id <appId>", "Container app id when --surface-model mobile_module is used").option("--profile <profile>", "First-party scaffold profile such as spatial-assets").option("--with-d1", "Declare a D1 binding in the first-party scaffold").option("--with-r2", "Declare an R2 binding in the first-party scaffold").option("--with-queue", "Declare a Queue binding in the first-party scaffold").option("--with-workflow", "Declare a Workflow binding in the first-party scaffold").option("--with-container-processing", "Declare container-processing metadata in CloudControl").option("--home-path <path>", "Authenticated home route (default: /dashboard)").option("--home-label <label>", "Home navigation label (default: Dashboard)").option("--navigation-mode <mode>", "Product navigation: sidebar, topbar, or none").option("--workspace-switcher <mode>", "Workspace context selector: required, optional, or off").option("--ui-template <template>", "Shared UI starter: operations, index, workbench, or minimal").option("--tagline <text>", "Product-facing landing and home description").option("--write", "Write the generated scaffold files to disk").option("--repo-root <path>", "Directory where scaffold files should be written").option("--force", "Overwrite existing scaffold files when --write is set").option("--with-api", "Include the API/service baseline").option("--no-api", "Do not include API/service requirements").option("--no-browser", "Do not include browser-shell requirements").option(
|
|
10424
10714
|
"--provision",
|
|
10425
10715
|
"Provision the generated first-party app into Topolo Developers and Auth using the authenticated Topolo admin session"
|
|
10426
10716
|
).option(
|
|
@@ -10460,6 +10750,7 @@ function registerPlatformCommand(program2) {
|
|
|
10460
10750
|
homeLabel: opts.homeLabel,
|
|
10461
10751
|
navigationMode: opts.navigationMode,
|
|
10462
10752
|
workspaceSwitcher: opts.workspaceSwitcher,
|
|
10753
|
+
uiTemplate: opts.uiTemplate,
|
|
10463
10754
|
tagline: opts.tagline
|
|
10464
10755
|
});
|
|
10465
10756
|
if (opts.workflowOnly && !payload.ciWorkflow) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"topoloPublishedVersion": "0.5.0",
|
|
4
5
|
"description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
@@ -11,10 +12,14 @@
|
|
|
11
12
|
"skills"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
15
|
+
"build:sdk": "pnpm --dir ../topolo-sdk build",
|
|
16
|
+
"prebuild": "pnpm run build:sdk",
|
|
14
17
|
"build": "tsup",
|
|
15
18
|
"dev": "tsup --watch",
|
|
19
|
+
"pretypecheck": "pnpm run build:sdk",
|
|
16
20
|
"typecheck": "tsc --noEmit",
|
|
17
21
|
"start": "node dist/index.js",
|
|
22
|
+
"pretest": "pnpm run build:sdk",
|
|
18
23
|
"test": "vitest run",
|
|
19
24
|
"test:watch": "vitest",
|
|
20
25
|
"coverage": "vitest run --coverage"
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
"commander": "^12.1.0"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
26
|
-
"@topolo/sdk": "
|
|
31
|
+
"@topolo/sdk": "file:../topolo-sdk",
|
|
27
32
|
"@types/node": "^20.12.0",
|
|
28
33
|
"@vitest/coverage-v8": "^2.1.0",
|
|
29
34
|
"tsup": "^8.0.0",
|