@namostack/cli 0.0.1 → 0.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@namostack/cli",
3
- "version": "0.0.1",
4
- "description": "Not released yet: see https://docs.namostack.com",
5
- "license": "MIT"
6
- }
3
+ "version": "0.1.0-rc.2",
4
+ "description": "Create, live test and publish namostack plugins.",
5
+ "license": "MIT",
6
+ "homepage": "https://docs.namostack.com",
7
+ "keywords": [
8
+ "namostack",
9
+ "plugin",
10
+ "cli"
11
+ ],
12
+ "type": "module",
13
+ "bin": {
14
+ "namostack": "./dist/index.mjs"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "templates",
19
+ "!templates/*/node_modules",
20
+ "!templates/*/.turbo",
21
+ "!templates/*/dist"
22
+ ],
23
+ "engines": {
24
+ "node": "^20.19.0 || >=22.12.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@clack/prompts": "^1.8.1",
31
+ "citty": "^0.2.2",
32
+ "picocolors": "^1.1.1",
33
+ "semver": "^7.8.5",
34
+ "tar": "^7.5.22",
35
+ "vite": "^8.3.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^24.13.3",
39
+ "@types/semver": "^7.8.0",
40
+ "tsdown": "^0.23.1",
41
+ "typescript": "~6.0.2",
42
+ "@namostack/api-client": "0.0.0",
43
+ "@namostack/config": "0.0.0",
44
+ "@namostack/types": "0.0.0",
45
+ "@namostack/tsconfig": "0.0.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsdown",
49
+ "lint": "oxlint",
50
+ "typecheck": "tsc -b"
51
+ }
52
+ }
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "template-react",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "typecheck": "tsc"
8
+ },
9
+ "dependencies": {
10
+ "@fontsource-variable/inter": "^5.3.0",
11
+ "@fontsource-variable/source-serif-4": "^5.3.1",
12
+ "@juliensouki/seedui": "0.0.4",
13
+ "@namostack/plugin-sdk": "workspace:*",
14
+ "react": "^19.2.8",
15
+ "react-dom": "^19.2.8",
16
+ "styled-components": "^6.1.0"
17
+ },
18
+ "devDependencies": {
19
+ "@types/react": "^19.2.18",
20
+ "@types/react-dom": "^19.2.7",
21
+ "@vitejs/plugin-react": "^6.1.1",
22
+ "typescript": "~6.0.2",
23
+ "vite": "^8.3.0"
24
+ }
25
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="14" fill="#6344e5" />
3
+ <path d="M22 16h13a11 11 0 0 1 0 22h-6v10h-7zm7 6v10h6a5 5 0 0 0 0-10z" fill="#fff" fill-rule="evenodd" />
4
+ </svg>
@@ -0,0 +1,84 @@
1
+ import { Text } from '@juliensouki/seedui';
2
+ import { styled } from '@juliensouki/seedui/sc';
3
+ import { useEffect, useState } from 'react';
4
+ import { Code, Muted } from './components/styles';
5
+ import { Counter } from './components/Counter';
6
+ import { Notify } from './components/Notify';
7
+ import { describeError } from './errors';
8
+ import { useNamostack } from './useNamostack';
9
+
10
+ const Page = styled.main((props) => ({
11
+ maxWidth: 560,
12
+ margin: '0 auto',
13
+ padding: '40px 24px',
14
+ display: 'flex',
15
+ flexDirection: 'column',
16
+ gap: 24,
17
+ [props.theme.breakpoints.down('sm')]: {
18
+ padding: '24px 16px',
19
+ },
20
+ }));
21
+
22
+ const Header = styled.header({
23
+ display: 'flex',
24
+ alignItems: 'center',
25
+ gap: 16,
26
+ });
27
+
28
+ const Icon = styled.img({
29
+ width: 56,
30
+ height: 56,
31
+ flexShrink: 0,
32
+ });
33
+
34
+ const Link = styled.a((props) => ({
35
+ color: props.theme.colors.primary.default,
36
+ textDecoration: 'none',
37
+ '&:hover': { textDecoration: 'underline' },
38
+ }));
39
+
40
+ export function App() {
41
+ const { context, user } = useNamostack();
42
+ const [displayName, setDisplayName] = useState<string>();
43
+ const [error, setError] = useState<string>();
44
+
45
+ useEffect(() => {
46
+ user
47
+ .getProfile()
48
+ .then((profile) => setDisplayName(profile.displayName))
49
+ .catch((caught: unknown) =>
50
+ setError(describeError(caught, 'user:profile:read')),
51
+ );
52
+ }, [user]);
53
+
54
+ return (
55
+ <Page>
56
+ <Header>
57
+ <Icon src={`${import.meta.env.BASE_URL}icon.svg`} alt="" />
58
+ <div>
59
+ <Text variant="h3">{context.name}</Text>
60
+ <Muted variant="p">
61
+ {displayName ? `Hi ${displayName}, ` : 'Hi, '}
62
+ this is your new plugin.
63
+ </Muted>
64
+ {error && <Muted variant="caption">{error}</Muted>}
65
+ </div>
66
+ </Header>
67
+
68
+ <Counter />
69
+ <Notify />
70
+
71
+ <Muted variant="caption">
72
+ Edit <Code>src/App.tsx</Code> and save to see it change, or{' '}
73
+ <Link
74
+ href="https://docs.namostack.com"
75
+ target="_blank"
76
+ rel="noreferrer"
77
+ >
78
+ read the docs
79
+ </Link>
80
+ .
81
+ </Muted>
82
+ </Page>
83
+ );
84
+ }
@@ -0,0 +1,41 @@
1
+ import { ThemeProvider } from '@juliensouki/seedui';
2
+ import { createGlobalStyle } from '@juliensouki/seedui/sc';
3
+ import type { Namostack } from '@namostack/plugin-sdk';
4
+ import { useEffect, useState, type ReactNode } from 'react';
5
+ import { NamostackContext } from './useNamostack';
6
+
7
+ // seedui doesn't reset the page; without this, browsers add an 8px margin.
8
+ const GlobalStyle = createGlobalStyle`
9
+ body {
10
+ margin: 0;
11
+ }
12
+ `;
13
+
14
+ interface NamostackProviderProps {
15
+ namostack: Namostack;
16
+ children: ReactNode;
17
+ }
18
+
19
+ /** Shares the connection with every component, and follows the app's theme. */
20
+ export function NamostackProvider({
21
+ namostack,
22
+ children,
23
+ }: NamostackProviderProps) {
24
+ const [mode, setMode] = useState(namostack.context.theme.mode);
25
+
26
+ useEffect(
27
+ () => namostack.onThemeChange((theme) => setMode(theme.mode)),
28
+ [namostack],
29
+ );
30
+
31
+ useEffect(() => {
32
+ document.documentElement.style.colorScheme = mode;
33
+ }, [mode]);
34
+
35
+ return (
36
+ <NamostackContext.Provider value={namostack}>
37
+ <GlobalStyle />
38
+ <ThemeProvider mode={mode}>{children}</ThemeProvider>
39
+ </NamostackContext.Provider>
40
+ );
41
+ }
@@ -0,0 +1,101 @@
1
+ import { Button, Text } from '@juliensouki/seedui';
2
+ import type { StoredDocument } from '@namostack/plugin-sdk';
3
+ import { useEffect, useState } from 'react';
4
+ import { describeError } from '../errors';
5
+ import { useNamostack } from '../useNamostack';
6
+ import { ExampleCard } from './ExampleCard';
7
+ import { Actions, Code } from './styles';
8
+
9
+ interface Clicks {
10
+ count: number;
11
+ }
12
+
13
+ type Action = 'increment' | 'reset';
14
+
15
+ /** A counter saved for the current user: it survives reloads and devices. */
16
+ export function Counter() {
17
+ const { data } = useNamostack();
18
+ const [clicks, setClicks] = useState<StoredDocument<Clicks> | null>();
19
+ const [pending, setPending] = useState<Action>();
20
+ const [error, setError] = useState<string>();
21
+
22
+ useEffect(() => {
23
+ data
24
+ .find<Clicks>('clicks', { limit: 1 })
25
+ .then(({ items }) => setClicks(items[0] ?? null))
26
+ .catch((caught: unknown) => {
27
+ setClicks(null);
28
+ setError(describeError(caught, 'storage'));
29
+ });
30
+ }, [data]);
31
+
32
+ const run = async (
33
+ action: Action,
34
+ change: () => Promise<StoredDocument<Clicks> | null>,
35
+ ) => {
36
+ setPending(action);
37
+ setError(undefined);
38
+
39
+ try {
40
+ setClicks(await change());
41
+ } catch (caught) {
42
+ setError(describeError(caught, 'storage'));
43
+ } finally {
44
+ setPending(undefined);
45
+ }
46
+ };
47
+
48
+ const increment = () =>
49
+ run('increment', () =>
50
+ clicks
51
+ ? data.update<Clicks>('clicks', clicks.id, { count: clicks.count + 1 })
52
+ : data.insert<Clicks>('clicks', { count: 1 }),
53
+ );
54
+
55
+ const reset = () =>
56
+ run('reset', async () => {
57
+ if (clicks) {
58
+ await data.delete('clicks', clicks.id);
59
+ }
60
+
61
+ return null;
62
+ });
63
+
64
+ const loading = clicks === undefined;
65
+
66
+ return (
67
+ <ExampleCard
68
+ title="Save data"
69
+ permission="storage"
70
+ error={error}
71
+ description={
72
+ <>
73
+ Each click is saved with <Code>data.update</Code>: reload, the count
74
+ stays.
75
+ </>
76
+ }
77
+ >
78
+ <Text variant="h2">{clicks?.count ?? 0}</Text>
79
+ <Actions>
80
+ <Button
81
+ size="md"
82
+ isLoading={pending === 'increment'}
83
+ disabled={loading || pending === 'reset'}
84
+ onClick={increment}
85
+ >
86
+ Click me
87
+ </Button>
88
+ <Button
89
+ size="md"
90
+ color="neutral"
91
+ variant="transparent"
92
+ isLoading={pending === 'reset'}
93
+ disabled={!clicks || pending === 'increment'}
94
+ onClick={reset}
95
+ >
96
+ Reset
97
+ </Button>
98
+ </Actions>
99
+ </ExampleCard>
100
+ );
101
+ }
@@ -0,0 +1,55 @@
1
+ import { Card, Tag, Text } from '@juliensouki/seedui';
2
+ import { styled } from '@juliensouki/seedui/sc';
3
+ import type { ReactNode } from 'react';
4
+ import { Muted } from './styles';
5
+
6
+ const Root = styled(Card)({
7
+ width: '100%',
8
+ boxSizing: 'border-box',
9
+ display: 'flex',
10
+ flexDirection: 'column',
11
+ gap: 16,
12
+ });
13
+
14
+ const Heading = styled.div({
15
+ display: 'flex',
16
+ alignItems: 'center',
17
+ justifyContent: 'space-between',
18
+ gap: 8,
19
+ });
20
+
21
+ const ErrorText = styled(Text)((props) => ({
22
+ color: props.theme.colors.error.default,
23
+ }));
24
+
25
+ interface ExampleCardProps {
26
+ title: string;
27
+ description: ReactNode;
28
+ /** The manifest permission the example needs. */
29
+ permission: string;
30
+ error?: string;
31
+ children: ReactNode;
32
+ }
33
+
34
+ /** One SDK feature: what it does, the permission it needs, and a way to try it. */
35
+ export function ExampleCard({
36
+ title,
37
+ description,
38
+ permission,
39
+ error,
40
+ children,
41
+ }: ExampleCardProps) {
42
+ return (
43
+ <Root variant="outlined" padding={20}>
44
+ <Heading>
45
+ <Text variant="h5">{title}</Text>
46
+ <Tag color="neutral" size="sm">
47
+ {permission}
48
+ </Tag>
49
+ </Heading>
50
+ <Muted variant="p">{description}</Muted>
51
+ {children}
52
+ {error && <ErrorText variant="caption">{error}</ErrorText>}
53
+ </Root>
54
+ );
55
+ }
@@ -0,0 +1,41 @@
1
+ import { Button } from '@juliensouki/seedui';
2
+ import { useState } from 'react';
3
+ import { describeError } from '../errors';
4
+ import { useNamostack } from '../useNamostack';
5
+ import { ExampleCard } from './ExampleCard';
6
+ import { Actions, Code } from './styles';
7
+
8
+ /** Shows a toast in the app, outside the plugin's frame. */
9
+ export function Notify() {
10
+ const { ui } = useNamostack();
11
+ const [error, setError] = useState<string>();
12
+
13
+ const notify = async () => {
14
+ setError(undefined);
15
+
16
+ try {
17
+ await ui.notify({ message: 'Hello from your plugin!', tone: 'success' });
18
+ } catch (caught) {
19
+ setError(describeError(caught, 'ui:notify'));
20
+ }
21
+ };
22
+
23
+ return (
24
+ <ExampleCard
25
+ title="Talk to the app"
26
+ permission="ui:notify"
27
+ error={error}
28
+ description={
29
+ <>
30
+ <Code>ui.notify</Code> shows a notification in the app.
31
+ </>
32
+ }
33
+ >
34
+ <Actions>
35
+ <Button size="md" color="neutral" onClick={notify}>
36
+ Say hello
37
+ </Button>
38
+ </Actions>
39
+ </ExampleCard>
40
+ );
41
+ }
@@ -0,0 +1,24 @@
1
+ import { Text } from '@juliensouki/seedui';
2
+ import { styled } from '@juliensouki/seedui/sc';
3
+
4
+ export const Actions = styled.div({
5
+ display: 'flex',
6
+ flexWrap: 'wrap',
7
+ gap: 8,
8
+ });
9
+
10
+ export const Code = styled.code((props) => ({
11
+ padding: '1px 6px',
12
+ borderRadius: props.theme.borderRadius(1),
13
+ background:
14
+ props.theme.colors.neutral[props.theme.mode === 'light' ? 200 : 400],
15
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
16
+ fontSize: '0.875em',
17
+ }));
18
+
19
+ export const Muted = styled(Text)((props) => ({
20
+ color:
21
+ props.theme.mode === 'light'
22
+ ? props.theme.colors.neutral[600]
23
+ : props.theme.colors.neutral[900],
24
+ }));
@@ -0,0 +1,10 @@
1
+ import { BridgeError } from '@namostack/plugin-sdk';
2
+
3
+ /** What to show when an SDK call fails; a missing permission says how to add it. */
4
+ export function describeError(error: unknown, permission: string): string {
5
+ if (error instanceof BridgeError && error.code === 'permission_denied') {
6
+ return `Add "${permission}" to the permissions in namostack.plugin.json.`;
7
+ }
8
+
9
+ return error instanceof BridgeError ? error.message : String(error);
10
+ }
@@ -0,0 +1,20 @@
1
+ import { connect } from '@namostack/plugin-sdk';
2
+ import { StrictMode } from 'react';
3
+ import { createRoot } from 'react-dom/client';
4
+ import { App } from './App';
5
+ import { NamostackProvider } from './NamostackProvider';
6
+
7
+ // seedui's fonts, as in the namostack app.
8
+ import '@fontsource-variable/inter';
9
+ import '@fontsource-variable/source-serif-4';
10
+
11
+ // Plugins only run inside the namostack app: anywhere else, this throws.
12
+ const namostack = await connect();
13
+
14
+ createRoot(document.getElementById('root')!).render(
15
+ <StrictMode>
16
+ <NamostackProvider namostack={namostack}>
17
+ <App />
18
+ </NamostackProvider>
19
+ </StrictMode>,
20
+ );
@@ -0,0 +1,16 @@
1
+ import type { Namostack } from '@namostack/plugin-sdk';
2
+ import { createContext, useContext } from 'react';
3
+
4
+ /** Provided by `NamostackProvider`. */
5
+ export const NamostackContext = createContext<Namostack | undefined>(undefined);
6
+
7
+ /** The data, user and UI methods of the namostack app. */
8
+ export function useNamostack(): Namostack {
9
+ const namostack = useContext(NamostackContext);
10
+
11
+ if (!namostack) {
12
+ throw new Error('useNamostack is only usable inside NamostackProvider.');
13
+ }
14
+
15
+ return namostack;
16
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2023",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "jsx": "react-jsx",
9
+ "types": ["vite/client"],
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "noEmit": true
13
+ },
14
+ "include": ["src", "vite.config.ts"]
15
+ }
@@ -0,0 +1,7 @@
1
+ import { namostackPlugin } from '@namostack/plugin-sdk/vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import { defineConfig } from 'vite';
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), namostackPlugin()],
7
+ });
@@ -0,0 +1,2 @@
1
+ node_modules
2
+ dist
@@ -0,0 +1,61 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{name}}</title>
7
+ </head>
8
+ <body>
9
+ <main class="page" hidden>
10
+ <header class="header">
11
+ <img class="icon" src="./icon.svg" alt="" />
12
+ <div>
13
+ <h1 class="h3" id="name"></h1>
14
+ <p class="p muted" id="greeting">Hi, this is your new plugin.</p>
15
+ <p class="caption muted" id="greeting-error" hidden></p>
16
+ </div>
17
+ </header>
18
+
19
+ <section class="card">
20
+ <div class="card-heading">
21
+ <h2 class="h5">Save data</h2>
22
+ <span class="tag">storage</span>
23
+ </div>
24
+ <p class="p muted">
25
+ Each click is saved with <code>data.update</code>: reload, the count
26
+ stays.
27
+ </p>
28
+ <p class="h2" id="count">0</p>
29
+ <div class="actions">
30
+ <button class="button" id="increment" disabled>Click me</button>
31
+ <button class="button button-transparent" id="reset" disabled>
32
+ Reset
33
+ </button>
34
+ </div>
35
+ <p class="caption error" id="count-error" hidden></p>
36
+ </section>
37
+
38
+ <section class="card">
39
+ <div class="card-heading">
40
+ <h2 class="h5">Talk to the app</h2>
41
+ <span class="tag">ui:notify</span>
42
+ </div>
43
+ <p class="p muted">
44
+ <code>ui.notify</code> shows a notification in the app.
45
+ </p>
46
+ <div class="actions">
47
+ <button class="button button-neutral" id="notify">Say hello</button>
48
+ </div>
49
+ <p class="caption error" id="notify-error" hidden></p>
50
+ </section>
51
+
52
+ <p class="caption muted">
53
+ Edit <code>src/main.ts</code> and save to see it change, or
54
+ <a href="https://docs.namostack.com" target="_blank" rel="noreferrer"
55
+ >read the docs</a
56
+ >.
57
+ </p>
58
+ </main>
59
+ <script type="module" src="/src/main.ts"></script>
60
+ </body>
61
+ </html>
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "template-vanilla",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "typecheck": "tsc"
8
+ },
9
+ "dependencies": {
10
+ "@fontsource-variable/inter": "^5.3.0",
11
+ "@fontsource-variable/source-serif-4": "^5.3.1",
12
+ "@namostack/plugin-sdk": "workspace:*"
13
+ },
14
+ "devDependencies": {
15
+ "typescript": "~6.0.2",
16
+ "vite": "^8.3.0"
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <rect width="64" height="64" rx="14" fill="#6344e5" />
3
+ <path d="M22 16h13a11 11 0 0 1 0 22h-6v10h-7zm7 6v10h6a5 5 0 0 0 0-10z" fill="#fff" fill-rule="evenodd" />
4
+ </svg>