@sequenceholdings/artifact-studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/api.d.ts +14 -0
  2. package/dist/api.js +49 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +129 -0
  5. package/dist/build.d.ts +12 -0
  6. package/dist/build.js +134 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +475 -0
  9. package/dist/config.d.ts +23 -0
  10. package/dist/config.js +48 -0
  11. package/dist/hash.d.ts +10 -0
  12. package/dist/hash.js +36 -0
  13. package/dist/manifest.d.ts +44 -0
  14. package/dist/manifest.js +45 -0
  15. package/dist/paths.d.ts +6 -0
  16. package/dist/paths.js +34 -0
  17. package/dist/project.d.ts +14 -0
  18. package/dist/project.js +76 -0
  19. package/dist/sdk.d.ts +60 -0
  20. package/dist/sdk.js +4 -0
  21. package/dist/templates/react-vite/CLAUDE.md +55 -0
  22. package/dist/templates/react-vite/artifact.bundle.yml +30 -0
  23. package/dist/templates/react-vite/index.html +12 -0
  24. package/dist/templates/react-vite/package.json +27 -0
  25. package/dist/templates/react-vite/src/App.tsx +54 -0
  26. package/dist/templates/react-vite/src/lib/api.ts +13 -0
  27. package/dist/templates/react-vite/src/main.tsx +18 -0
  28. package/dist/templates/react-vite/src/styles.css +3 -0
  29. package/dist/templates/react-vite/vite.config.ts +7 -0
  30. package/package.json +50 -0
  31. package/templates/react-vite/CLAUDE.md +55 -0
  32. package/templates/react-vite/artifact.bundle.yml +30 -0
  33. package/templates/react-vite/index.html +12 -0
  34. package/templates/react-vite/package.json +27 -0
  35. package/templates/react-vite/src/App.tsx +54 -0
  36. package/templates/react-vite/src/lib/api.ts +13 -0
  37. package/templates/react-vite/src/main.tsx +18 -0
  38. package/templates/react-vite/src/styles.css +3 -0
  39. package/templates/react-vite/vite.config.ts +7 -0
@@ -0,0 +1,6 @@
1
+ export declare const MAX_STUDIO_PATH_LENGTH = 512;
2
+ export declare class InvalidArtifactPathError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ export declare function normalizeArtifactPath(path: string): string;
6
+ export declare function validateArtifactPath(path: string): void;
package/dist/paths.js ADDED
@@ -0,0 +1,34 @@
1
+ export const MAX_STUDIO_PATH_LENGTH = 512;
2
+ const WINDOWS_DRIVE_RE = /^[a-zA-Z]:[\\/]/;
3
+ const CONTROL_CHAR_RE = /[\x00-\x1f]/;
4
+ export class InvalidArtifactPathError extends Error {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'InvalidArtifactPathError';
8
+ }
9
+ }
10
+ export function normalizeArtifactPath(path) {
11
+ const normalized = path.replace(/\\/g, '/').trim();
12
+ validateArtifactPath(normalized);
13
+ return normalized;
14
+ }
15
+ export function validateArtifactPath(path) {
16
+ if (!path)
17
+ throw new InvalidArtifactPathError('path cannot be empty');
18
+ if (path.length > MAX_STUDIO_PATH_LENGTH) {
19
+ throw new InvalidArtifactPathError(`path exceeds ${MAX_STUDIO_PATH_LENGTH} characters`);
20
+ }
21
+ if (path.startsWith('/'))
22
+ throw new InvalidArtifactPathError('absolute paths are not allowed');
23
+ if (WINDOWS_DRIVE_RE.test(path))
24
+ throw new InvalidArtifactPathError('Windows drive paths are not allowed');
25
+ if (CONTROL_CHAR_RE.test(path))
26
+ throw new InvalidArtifactPathError('control characters are not allowed in paths');
27
+ const segments = path.split('/');
28
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
29
+ throw new InvalidArtifactPathError('path segments cannot be empty, ".", or ".."');
30
+ }
31
+ if (segments.includes('node_modules')) {
32
+ throw new InvalidArtifactPathError('node_modules cannot be included in artifact source');
33
+ }
34
+ }
@@ -0,0 +1,14 @@
1
+ import { type ArtifactStudioManifest } from './manifest.js';
2
+ export interface ArtifactStudioSourceFile {
3
+ path: string;
4
+ content: string;
5
+ contentType: string;
6
+ }
7
+ export interface ArtifactStudioSource {
8
+ root: string;
9
+ manifest: ArtifactStudioManifest;
10
+ files: ArtifactStudioSourceFile[];
11
+ sourceHash: string;
12
+ }
13
+ export declare function hasArtifactStudioManifest(rootDir: string): boolean;
14
+ export declare function readArtifactStudioSource(rootDir: string): Promise<ArtifactStudioSource>;
@@ -0,0 +1,76 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { join, relative, resolve } from 'node:path';
4
+ import yaml from 'js-yaml';
5
+ import { computeSourceHash } from './hash.js';
6
+ import { parseArtifactStudioManifest } from './manifest.js';
7
+ import { normalizeArtifactPath } from './paths.js';
8
+ const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', '.next', '.artifact-studio']);
9
+ const TEXT_EXTENSIONS = new Set([
10
+ '.ts', '.tsx', '.js', '.jsx', '.json', '.css', '.html', '.md', '.yml', '.yaml', '.svg',
11
+ ]);
12
+ export function hasArtifactStudioManifest(rootDir) {
13
+ return existsSync(join(resolve(rootDir), 'artifact.bundle.yml'));
14
+ }
15
+ export async function readArtifactStudioSource(rootDir) {
16
+ const root = resolve(rootDir);
17
+ const rawManifest = yaml.load(await readFile(join(root, 'artifact.bundle.yml'), 'utf8'));
18
+ const manifest = parseArtifactStudioManifest(rawManifest);
19
+ const files = await collectSourceFiles(root);
20
+ if (!files.some((file) => file.path === manifest.artifact.entrypoint)) {
21
+ throw new Error(`Entrypoint file not found: ${manifest.artifact.entrypoint}`);
22
+ }
23
+ return {
24
+ root,
25
+ manifest,
26
+ files,
27
+ sourceHash: computeSourceHash({ files, manifest }),
28
+ };
29
+ }
30
+ async function collectSourceFiles(root) {
31
+ const files = [];
32
+ async function walk(dir) {
33
+ const entries = await readdir(dir, { withFileTypes: true });
34
+ for (const entry of entries) {
35
+ const absolute = join(dir, entry.name);
36
+ if (entry.isDirectory()) {
37
+ if (!IGNORED_DIRS.has(entry.name))
38
+ await walk(absolute);
39
+ continue;
40
+ }
41
+ if (!entry.isFile())
42
+ continue;
43
+ const path = normalizeArtifactPath(relative(root, absolute));
44
+ if (!isTextFile(path))
45
+ continue;
46
+ files.push({
47
+ path,
48
+ content: await readFile(absolute, 'utf8'),
49
+ contentType: inferContentType(path),
50
+ });
51
+ }
52
+ }
53
+ await walk(root);
54
+ return files.sort((a, b) => a.path.localeCompare(b.path));
55
+ }
56
+ function isTextFile(path) {
57
+ const ext = path.includes('.') ? path.slice(path.lastIndexOf('.')) : '';
58
+ return path === 'artifact.bundle.yml' || TEXT_EXTENSIONS.has(ext);
59
+ }
60
+ function inferContentType(path) {
61
+ if (path.endsWith('.tsx') || path.endsWith('.ts'))
62
+ return 'text/typescript';
63
+ if (path.endsWith('.jsx') || path.endsWith('.js'))
64
+ return 'text/javascript';
65
+ if (path.endsWith('.css'))
66
+ return 'text/css';
67
+ if (path.endsWith('.json'))
68
+ return 'application/json';
69
+ if (path.endsWith('.yml') || path.endsWith('.yaml'))
70
+ return 'text/yaml';
71
+ if (path.endsWith('.html'))
72
+ return 'text/html';
73
+ if (path.endsWith('.svg'))
74
+ return 'image/svg+xml';
75
+ return 'text/plain';
76
+ }
package/dist/sdk.d.ts ADDED
@@ -0,0 +1,60 @@
1
+ export interface SequenceApiFetchOptions {
2
+ method?: string;
3
+ body?: unknown;
4
+ headers?: Record<string, string>;
5
+ }
6
+ export interface SequenceApiFetchResponse<TBody = unknown> {
7
+ ok: boolean;
8
+ status: number;
9
+ body: TBody;
10
+ }
11
+ export interface SequenceApi {
12
+ fetch<TBody = unknown>(path: string, options?: SequenceApiFetchOptions): Promise<SequenceApiFetchResponse<TBody>>;
13
+ stream(path: string, options?: SequenceApiFetchOptions): Promise<ReadableStream<Uint8Array>>;
14
+ get<TBody = unknown>(path: string): Promise<TBody>;
15
+ post<TBody = unknown>(path: string, body?: unknown): Promise<TBody>;
16
+ put<TBody = unknown>(path: string, body?: unknown): Promise<TBody>;
17
+ patch<TBody = unknown>(path: string, body?: unknown): Promise<TBody>;
18
+ delete<TBody = unknown>(path: string, body?: unknown): Promise<TBody>;
19
+ }
20
+ export interface SequenceFunctions {
21
+ invoke<TOutput = unknown>(id: string, params?: Record<string, unknown>): Promise<TOutput>;
22
+ }
23
+ export interface SequenceUser {
24
+ current(): Promise<{
25
+ email: string;
26
+ name: string;
27
+ id: string;
28
+ }>;
29
+ }
30
+ export interface SequenceEnvironment {
31
+ current(): Promise<{
32
+ name: string;
33
+ url: string;
34
+ }>;
35
+ }
36
+ export interface SequenceAuth {
37
+ hasPermission(permission: string): Promise<boolean>;
38
+ getPermissions(): Promise<string[]>;
39
+ }
40
+ export interface SequenceFlags {
41
+ isEnabled(flagName: string): Promise<boolean>;
42
+ }
43
+ export interface SequenceArtifactSdk {
44
+ api: SequenceApi;
45
+ ai: {
46
+ fetch(path: string, options?: SequenceApiFetchOptions): Promise<Response>;
47
+ };
48
+ functions: SequenceFunctions;
49
+ user: SequenceUser;
50
+ env: SequenceEnvironment;
51
+ auth: SequenceAuth;
52
+ flags: SequenceFlags;
53
+ }
54
+ declare global {
55
+ interface Window {
56
+ seq: SequenceArtifactSdk;
57
+ }
58
+ }
59
+ export declare const seq: SequenceArtifactSdk;
60
+ export default seq;
package/dist/sdk.js ADDED
@@ -0,0 +1,4 @@
1
+ export const seq = typeof window !== 'undefined'
2
+ ? window.seq
3
+ : undefined;
4
+ export default seq;
@@ -0,0 +1,55 @@
1
+ # Sequence Artifact Studio app
2
+
3
+ This is a sandboxed iframe SPA that runs inside Atlas and talks to Atlas backend routes through a postMessage bridge exposed as `seq.api.*` from `@sequenceholdings/artifact-studio`. No direct network access, no API keys in the bundle — everything goes through the bridge, and every endpoint the artifact uses must be declared in `artifact.bundle.yml`.
4
+
5
+ ## Three rules that bite
6
+
7
+ 1. **Envelope unwrap.** Atlas wraps every CRM-style response in `{ success: true, data: T }`. The bridge passes that envelope through raw — it does **not** unwrap automatically. Route every read/write through `unwrap<T>()` in `src/lib/api.ts`:
8
+ ```ts
9
+ import { unwrap, seq } from './lib/api'
10
+ const customer = await unwrap<Customer>(seq.api.get(`/api/crm/customers/${id}`))
11
+ ```
12
+ For endpoints with extra envelope fields (pagination `meta`, warnings), drop to `seq.api.fetch` and read `res.body` directly.
13
+
14
+ 2. **FormData blocker.** `seq.api.fetch` `JSON.stringify`s any non-string `body` value, which silently turns `FormData` into `"{}"`. For uploads, base64-encode the bytes and POST as JSON, or patch the Atlas bridge first.
15
+
16
+ 3. **Capability matcher: exact path or `/*` prefix wildcard only.** `{id}` template forms do **not** match. List both the bare path and the prefix wildcard in `artifact.bundle.yml`:
17
+ ```yaml
18
+ - /api/crm/customers
19
+ - /api/crm/customers/*
20
+ ```
21
+
22
+ ## Prerequisite
23
+
24
+ `artifact-studio init` requires the Sequence monorepo checked out locally. The CLI rewrites `"@sequenceholdings/atlas-ui": "workspace:*"` and the `@sequenceholdings/artifact-studio` entry in this `package.json` to local `link:` paths against the monorepo's `shared/services/atlas-ui` and `shared/services/artifact-studio`. Without the monorepo the rewrite no-ops, and `pnpm install` then fails on the unresolved `workspace:*` — those two packages aren't published to the public npm registry yet. Run init from inside the monorepo, e.g. `pnpm --dir atlas artifact-studio init <dir>`.
25
+
26
+ ## Package manager: pnpm
27
+
28
+ This scaffold declares `"packageManager": "pnpm@10.28.2"` and the CLI writes both `@sequenceholdings/atlas-ui` and `@sequenceholdings/artifact-studio` as **`link:` deps** (not `file:`). `link:` is pnpm-specific: it tells pnpm to symlink the package without re-resolving its transitive `workspace:*` deps. That matters because the linked `@sequenceholdings/artifact-studio` package still carries `"@sequenceholdings/atlas-ui": "workspace:*"` in its own `package.json`; with `file:` pnpm would try to resolve that protocol against the standalone scaffold and fail with `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`. Always `pnpm install` — npm doesn't understand `link:`.
29
+
30
+ ## Key files
31
+
32
+ | File | Role |
33
+ |---|---|
34
+ | `artifact.bundle.yml` | Capability manifest — every API path the artifact calls, exact + `/*` |
35
+ | `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
36
+ | `src/App.tsx` | Entrypoint UI |
37
+ | `src/lib/api.ts` | `unwrap<T>` envelope helper + re-export of `seq` |
38
+ | `src/styles.css` | Tailwind v4 + `@sequenceholdings/atlas-ui/tokens.css` |
39
+ | `vite.config.ts` | Vite + React + Tailwind v4 plugin |
40
+
41
+ ## Defaults
42
+
43
+ - UI: prefer `@sequenceholdings/atlas-ui` primitives (`Button`, `Card`, `Input`, `Dialog`, `KpiTile`, `Switch`, …) over raw Tailwind. `Select`, `Checkbox`, and `DropdownMenu` are not exported — use native `<select>`, `Switch`, and `Popover` respectively.
44
+ - Routing: `HashRouter`. Mount routes at `/`, not at the Atlas app prefix.
45
+ - Data: React Query is wired in `src/main.tsx`. Use `useQuery` / `useMutation` against `unwrap(seq.api.*)`.
46
+ - Icons: do **not** install `@tabler/icons-react` — it won't resolve from the artifact's local `node_modules`. Inline SVG components in `src/icons.tsx`.
47
+
48
+ ## Bridge methods
49
+
50
+ Two primitives on `seq.api`:
51
+
52
+ - `seq.api.fetch(path, options?)` returns `{ ok, status, body }` — does not throw on non-2xx. Use when you need the status (e.g. handle 409) or the raw envelope with extra fields like `meta`.
53
+ - `seq.api.request(method, path, body?)` throws on non-2xx and returns parsed JSON. Sugar wrappers `seq.api.get / .post / .patch / .delete` are thin shells around `request`, so they also throw.
54
+
55
+ Wrap reads/writes that should auto-throw through `unwrap()` in `src/lib/api.ts`. Drop to `seq.api.fetch` when you need pagination `meta` or status inspection.
@@ -0,0 +1,30 @@
1
+ bundle:
2
+ name: {{slug}}
3
+ schema_version: 1
4
+
5
+ artifact:
6
+ project_id: {{slug}}
7
+ title: "{{title}}"
8
+ description: "A Git-backed Artifact Studio app"
9
+ type: react
10
+ entrypoint: src/App.tsx
11
+
12
+ runtime:
13
+ sdk: sequence
14
+ react: 18
15
+
16
+ capabilities:
17
+ data:
18
+ read: []
19
+ write: []
20
+ api:
21
+ read:
22
+ - /api/health
23
+ write: []
24
+ functions:
25
+ invoke: []
26
+
27
+ targets:
28
+ staging:
29
+ url: https://staging.atlas.seqholdings.com
30
+ visibility: private
@@ -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>Artifact Studio App</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,27 @@
1
+ {
2
+ "name": "{{slug}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "packageManager": "pnpm@10.28.2",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@sequenceholdings/artifact-studio": "workspace:*",
13
+ "@sequenceholdings/atlas-ui": "workspace:*",
14
+ "@tanstack/react-query": "^4.36.1",
15
+ "react": "^18.2.0",
16
+ "react-dom": "^18.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@tailwindcss/postcss": "^4.2.2",
20
+ "@tailwindcss/vite": "^4.2.2",
21
+ "@vitejs/plugin-react": "^5.0.0",
22
+ "tailwindcss": "^4.2.2",
23
+ "tw-animate-css": "^1.4.0",
24
+ "typescript": "^5.6.0",
25
+ "vite": "8.0.8"
26
+ }
27
+ }
@@ -0,0 +1,54 @@
1
+ import { seq } from '@sequenceholdings/artifact-studio'
2
+ import {
3
+ Button,
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardContent,
8
+ Badge,
9
+ } from '@sequenceholdings/atlas-ui'
10
+
11
+ const CARDS = [
12
+ { label: 'Source model', value: 'Vite React' },
13
+ { label: 'Runtime', value: 'Atlas iframe' },
14
+ { label: 'Release mode', value: 'Immutable' },
15
+ ]
16
+
17
+ export default function App() {
18
+ async function pingAtlas() {
19
+ const response = await seq.api.get<{ ok?: boolean }>('/api/health')
20
+ alert(`Atlas responded: ${JSON.stringify(response)}`)
21
+ }
22
+
23
+ return (
24
+ <main className="min-h-screen bg-background p-10">
25
+ <p className="text-xs font-extrabold tracking-widest uppercase text-primary mb-2">
26
+ Artifact Studio
27
+ </p>
28
+ <h1 className="text-lg font-bold tracking-tight">Build on Atlas</h1>
29
+ <p className="text-muted-foreground leading-relaxed max-w-2xl mt-2">
30
+ This app uses Atlas&apos;s shared UI component library. Import any
31
+ component from <code className="text-sm font-mono">@sequenceholdings/atlas-ui</code>.
32
+ </p>
33
+
34
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-sm mt-7 max-w-3xl">
35
+ {CARDS.map((card) => (
36
+ <Card key={card.label}>
37
+ <CardHeader>
38
+ <CardTitle className="text-sm text-muted-foreground">
39
+ {card.label}
40
+ </CardTitle>
41
+ </CardHeader>
42
+ <CardContent>
43
+ <Badge variant="secondary">{card.value}</Badge>
44
+ </CardContent>
45
+ </Card>
46
+ ))}
47
+ </div>
48
+
49
+ <Button className="mt-6" onClick={pingAtlas}>
50
+ Test Atlas API bridge
51
+ </Button>
52
+ </main>
53
+ )
54
+ }
@@ -0,0 +1,13 @@
1
+ import { seq } from '@sequenceholdings/artifact-studio'
2
+
3
+ export async function unwrap<T>(p: Promise<unknown>): Promise<T> {
4
+ const json = await p
5
+ if (json && typeof json === 'object' && 'success' in json && 'data' in json) {
6
+ const env = json as { success: boolean; data: T; error?: string }
7
+ if (!env.success) throw new Error(env.error ?? 'API call failed')
8
+ return env.data
9
+ }
10
+ return json as T
11
+ }
12
+
13
+ export { seq }
@@ -0,0 +1,18 @@
1
+ import React from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import { PortalContainerProvider } from '@sequenceholdings/atlas-ui'
5
+ import App from './App'
6
+ import './styles.css'
7
+
8
+ const queryClient = new QueryClient()
9
+
10
+ createRoot(document.getElementById('root')!).render(
11
+ <React.StrictMode>
12
+ <QueryClientProvider client={queryClient}>
13
+ <PortalContainerProvider>
14
+ <App />
15
+ </PortalContainerProvider>
16
+ </QueryClientProvider>
17
+ </React.StrictMode>,
18
+ )
@@ -0,0 +1,3 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "@sequenceholdings/atlas-ui/tokens.css";
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), tailwindcss()],
7
+ })
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@sequenceholdings/artifact-studio",
3
+ "version": "0.1.0",
4
+ "description": "CLI and SDK types for building Artifact Studio apps",
5
+ "type": "module",
6
+ "bin": {
7
+ "artifact-studio": "./dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/sdk.d.ts",
12
+ "default": "./dist/sdk.js"
13
+ },
14
+ "./cli": {
15
+ "types": "./dist/cli.d.ts",
16
+ "default": "./dist/cli.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "templates"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public",
25
+ "registry": "https://registry.npmjs.org"
26
+ },
27
+ "dependencies": {
28
+ "@tailwindcss/postcss": "^4.1.8",
29
+ "@tailwindcss/vite": "^4.1.8",
30
+ "js-yaml": "^4.1.1",
31
+ "react": "^18.2.0",
32
+ "react-dom": "^18.2.0",
33
+ "tailwindcss": "^4.1.8",
34
+ "tw-animate-css": "^1.4.0",
35
+ "vite": "8.0.8",
36
+ "zod": "^4.1.13",
37
+ "@sequenceholdings/atlas-ui": "0.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/js-yaml": "^4.0.9",
41
+ "@types/node": "^22.0.0",
42
+ "typescript": "^5.6.0",
43
+ "vitest": "^4.1.2"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc && node scripts/copy-templates.mjs",
47
+ "type-check": "tsc --noEmit",
48
+ "test": "vitest run"
49
+ }
50
+ }
@@ -0,0 +1,55 @@
1
+ # Sequence Artifact Studio app
2
+
3
+ This is a sandboxed iframe SPA that runs inside Atlas and talks to Atlas backend routes through a postMessage bridge exposed as `seq.api.*` from `@sequenceholdings/artifact-studio`. No direct network access, no API keys in the bundle — everything goes through the bridge, and every endpoint the artifact uses must be declared in `artifact.bundle.yml`.
4
+
5
+ ## Three rules that bite
6
+
7
+ 1. **Envelope unwrap.** Atlas wraps every CRM-style response in `{ success: true, data: T }`. The bridge passes that envelope through raw — it does **not** unwrap automatically. Route every read/write through `unwrap<T>()` in `src/lib/api.ts`:
8
+ ```ts
9
+ import { unwrap, seq } from './lib/api'
10
+ const customer = await unwrap<Customer>(seq.api.get(`/api/crm/customers/${id}`))
11
+ ```
12
+ For endpoints with extra envelope fields (pagination `meta`, warnings), drop to `seq.api.fetch` and read `res.body` directly.
13
+
14
+ 2. **FormData blocker.** `seq.api.fetch` `JSON.stringify`s any non-string `body` value, which silently turns `FormData` into `"{}"`. For uploads, base64-encode the bytes and POST as JSON, or patch the Atlas bridge first.
15
+
16
+ 3. **Capability matcher: exact path or `/*` prefix wildcard only.** `{id}` template forms do **not** match. List both the bare path and the prefix wildcard in `artifact.bundle.yml`:
17
+ ```yaml
18
+ - /api/crm/customers
19
+ - /api/crm/customers/*
20
+ ```
21
+
22
+ ## Prerequisite
23
+
24
+ `artifact-studio init` requires the Sequence monorepo checked out locally. The CLI rewrites `"@sequenceholdings/atlas-ui": "workspace:*"` and the `@sequenceholdings/artifact-studio` entry in this `package.json` to local `link:` paths against the monorepo's `shared/services/atlas-ui` and `shared/services/artifact-studio`. Without the monorepo the rewrite no-ops, and `pnpm install` then fails on the unresolved `workspace:*` — those two packages aren't published to the public npm registry yet. Run init from inside the monorepo, e.g. `pnpm --dir atlas artifact-studio init <dir>`.
25
+
26
+ ## Package manager: pnpm
27
+
28
+ This scaffold declares `"packageManager": "pnpm@10.28.2"` and the CLI writes both `@sequenceholdings/atlas-ui` and `@sequenceholdings/artifact-studio` as **`link:` deps** (not `file:`). `link:` is pnpm-specific: it tells pnpm to symlink the package without re-resolving its transitive `workspace:*` deps. That matters because the linked `@sequenceholdings/artifact-studio` package still carries `"@sequenceholdings/atlas-ui": "workspace:*"` in its own `package.json`; with `file:` pnpm would try to resolve that protocol against the standalone scaffold and fail with `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`. Always `pnpm install` — npm doesn't understand `link:`.
29
+
30
+ ## Key files
31
+
32
+ | File | Role |
33
+ |---|---|
34
+ | `artifact.bundle.yml` | Capability manifest — every API path the artifact calls, exact + `/*` |
35
+ | `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
36
+ | `src/App.tsx` | Entrypoint UI |
37
+ | `src/lib/api.ts` | `unwrap<T>` envelope helper + re-export of `seq` |
38
+ | `src/styles.css` | Tailwind v4 + `@sequenceholdings/atlas-ui/tokens.css` |
39
+ | `vite.config.ts` | Vite + React + Tailwind v4 plugin |
40
+
41
+ ## Defaults
42
+
43
+ - UI: prefer `@sequenceholdings/atlas-ui` primitives (`Button`, `Card`, `Input`, `Dialog`, `KpiTile`, `Switch`, …) over raw Tailwind. `Select`, `Checkbox`, and `DropdownMenu` are not exported — use native `<select>`, `Switch`, and `Popover` respectively.
44
+ - Routing: `HashRouter`. Mount routes at `/`, not at the Atlas app prefix.
45
+ - Data: React Query is wired in `src/main.tsx`. Use `useQuery` / `useMutation` against `unwrap(seq.api.*)`.
46
+ - Icons: do **not** install `@tabler/icons-react` — it won't resolve from the artifact's local `node_modules`. Inline SVG components in `src/icons.tsx`.
47
+
48
+ ## Bridge methods
49
+
50
+ Two primitives on `seq.api`:
51
+
52
+ - `seq.api.fetch(path, options?)` returns `{ ok, status, body }` — does not throw on non-2xx. Use when you need the status (e.g. handle 409) or the raw envelope with extra fields like `meta`.
53
+ - `seq.api.request(method, path, body?)` throws on non-2xx and returns parsed JSON. Sugar wrappers `seq.api.get / .post / .patch / .delete` are thin shells around `request`, so they also throw.
54
+
55
+ Wrap reads/writes that should auto-throw through `unwrap()` in `src/lib/api.ts`. Drop to `seq.api.fetch` when you need pagination `meta` or status inspection.
@@ -0,0 +1,30 @@
1
+ bundle:
2
+ name: {{slug}}
3
+ schema_version: 1
4
+
5
+ artifact:
6
+ project_id: {{slug}}
7
+ title: "{{title}}"
8
+ description: "A Git-backed Artifact Studio app"
9
+ type: react
10
+ entrypoint: src/App.tsx
11
+
12
+ runtime:
13
+ sdk: sequence
14
+ react: 18
15
+
16
+ capabilities:
17
+ data:
18
+ read: []
19
+ write: []
20
+ api:
21
+ read:
22
+ - /api/health
23
+ write: []
24
+ functions:
25
+ invoke: []
26
+
27
+ targets:
28
+ staging:
29
+ url: https://staging.atlas.seqholdings.com
30
+ visibility: private
@@ -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>Artifact Studio App</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,27 @@
1
+ {
2
+ "name": "{{slug}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "packageManager": "pnpm@10.28.2",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@sequenceholdings/artifact-studio": "workspace:*",
13
+ "@sequenceholdings/atlas-ui": "workspace:*",
14
+ "@tanstack/react-query": "^4.36.1",
15
+ "react": "^18.2.0",
16
+ "react-dom": "^18.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@tailwindcss/postcss": "^4.2.2",
20
+ "@tailwindcss/vite": "^4.2.2",
21
+ "@vitejs/plugin-react": "^5.0.0",
22
+ "tailwindcss": "^4.2.2",
23
+ "tw-animate-css": "^1.4.0",
24
+ "typescript": "^5.6.0",
25
+ "vite": "8.0.8"
26
+ }
27
+ }