@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
package/dist/api.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export interface ApiClientOptions {
2
+ baseUrl: string;
3
+ token: string;
4
+ }
5
+ export declare function getJson<T>({ baseUrl, token, path }: ApiClientOptions & {
6
+ path: string;
7
+ }): Promise<T>;
8
+ export declare function getJsonOr404<T>({ baseUrl, token, path }: ApiClientOptions & {
9
+ path: string;
10
+ }): Promise<T | null>;
11
+ export declare function postJson<T>({ baseUrl, token, path, body, }: ApiClientOptions & {
12
+ path: string;
13
+ body?: unknown;
14
+ }): Promise<T>;
package/dist/api.js ADDED
@@ -0,0 +1,49 @@
1
+ const MAX_503_RETRIES = 5;
2
+ const DEFAULT_RETRY_AFTER_SECONDS = 2;
3
+ async function fetchWith503Retry(input, init) {
4
+ let attempt = 0;
5
+ while (true) {
6
+ const response = await fetch(input, init);
7
+ if (response.status !== 503 || attempt >= MAX_503_RETRIES)
8
+ return response;
9
+ const retryAfter = Number(response.headers.get('Retry-After')) || DEFAULT_RETRY_AFTER_SECONDS;
10
+ attempt += 1;
11
+ console.warn(`[artifact-studio] backend warming up (503); retrying in ${retryAfter}s (attempt ${attempt}/${MAX_503_RETRIES})`);
12
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
13
+ }
14
+ }
15
+ export async function getJson({ baseUrl, token, path }) {
16
+ const response = await fetchWith503Retry(`${baseUrl}${path}`, {
17
+ headers: { Authorization: `Bearer ${token}` },
18
+ });
19
+ if (!response.ok)
20
+ throw new Error(await responseError(response));
21
+ return response.json();
22
+ }
23
+ export async function getJsonOr404({ baseUrl, token, path }) {
24
+ const response = await fetchWith503Retry(`${baseUrl}${path}`, {
25
+ headers: { Authorization: `Bearer ${token}` },
26
+ });
27
+ if (response.status === 404)
28
+ return null;
29
+ if (!response.ok)
30
+ throw new Error(await responseError(response));
31
+ return response.json();
32
+ }
33
+ export async function postJson({ baseUrl, token, path, body, }) {
34
+ const response = await fetchWith503Retry(`${baseUrl}${path}`, {
35
+ method: 'POST',
36
+ headers: {
37
+ Authorization: `Bearer ${token}`,
38
+ 'Content-Type': 'application/json',
39
+ },
40
+ body: body === undefined ? undefined : JSON.stringify(body),
41
+ });
42
+ if (!response.ok)
43
+ throw new Error(await responseError(response));
44
+ return response.json();
45
+ }
46
+ async function responseError(response) {
47
+ const body = await response.json().catch(() => ({ detail: `HTTP ${response.status}` }));
48
+ return body.detail ?? body.error ?? `HTTP ${response.status}`;
49
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function loginWithPkce(): Promise<void>;
2
+ export declare function getAccessToken(): Promise<string | null>;
package/dist/auth.js ADDED
@@ -0,0 +1,129 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { spawn } from 'node:child_process';
4
+ import { writeTokenConfig, readTokenConfig } from './config.js';
5
+ const REDIRECT_PORT = 5099;
6
+ const REDIRECT_URI = `http://localhost:${REDIRECT_PORT}`;
7
+ const DEFAULT_AUTH0_DOMAIN = 'dev-n1t8ts403fp8oyxp.us.auth0.com';
8
+ const DEFAULT_AUTH0_CLIENT_ID = 'GD9riCDWocfc66odpWBjwBiX43qqAX8r';
9
+ const DEFAULT_AUTH0_AUDIENCE = 'https://api.studio.com';
10
+ function envOr(name, fallback) {
11
+ return process.env[name]?.trim() || fallback;
12
+ }
13
+ function readAuth0Config() {
14
+ return {
15
+ domain: envOr('ARTIFACT_STUDIO_AUTH0_DOMAIN', DEFAULT_AUTH0_DOMAIN),
16
+ clientId: envOr('ARTIFACT_STUDIO_AUTH0_CLIENT_ID', DEFAULT_AUTH0_CLIENT_ID),
17
+ audience: envOr('ARTIFACT_STUDIO_AUTH0_AUDIENCE', DEFAULT_AUTH0_AUDIENCE),
18
+ };
19
+ }
20
+ function base64Url(input) {
21
+ return input.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
22
+ }
23
+ function openBrowser(url) {
24
+ const command = process.platform === 'darwin'
25
+ ? 'open'
26
+ : process.platform === 'win32'
27
+ ? 'cmd'
28
+ : 'xdg-open';
29
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
30
+ const child = spawn(command, args, { stdio: 'ignore', detached: true });
31
+ child.unref();
32
+ }
33
+ export async function loginWithPkce() {
34
+ const config = readAuth0Config();
35
+ const verifier = base64Url(randomBytes(32));
36
+ const challenge = base64Url(createHash('sha256').update(verifier).digest());
37
+ const authUrl = new URL(`https://${config.domain}/authorize`);
38
+ authUrl.searchParams.set('response_type', 'code');
39
+ authUrl.searchParams.set('client_id', config.clientId);
40
+ authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
41
+ authUrl.searchParams.set('scope', 'openid profile email offline_access');
42
+ authUrl.searchParams.set('audience', config.audience);
43
+ authUrl.searchParams.set('code_challenge', challenge);
44
+ authUrl.searchParams.set('code_challenge_method', 'S256');
45
+ const code = await waitForCode(authUrl.toString());
46
+ const tokenResponse = await fetch(`https://${config.domain}/oauth/token`, {
47
+ method: 'POST',
48
+ headers: { 'Content-Type': 'application/json' },
49
+ body: JSON.stringify({
50
+ grant_type: 'authorization_code',
51
+ client_id: config.clientId,
52
+ code,
53
+ redirect_uri: REDIRECT_URI,
54
+ code_verifier: verifier,
55
+ }),
56
+ });
57
+ if (!tokenResponse.ok) {
58
+ throw new Error(`Auth0 token exchange failed: ${await tokenResponse.text()}`);
59
+ }
60
+ const tokens = await tokenResponse.json();
61
+ if (!tokens.refresh_token) {
62
+ throw new Error('No refresh token returned. Ensure Auth0 offline access is enabled.');
63
+ }
64
+ await writeTokenConfig({
65
+ accessToken: tokens.access_token,
66
+ refreshToken: tokens.refresh_token,
67
+ expiresAt: Date.now() + (tokens.expires_in ?? 86400) * 1000,
68
+ });
69
+ }
70
+ export async function getAccessToken() {
71
+ const tokens = await readTokenConfig();
72
+ if (!tokens.accessToken && !tokens.refreshToken)
73
+ return null;
74
+ if (tokens.accessToken && tokens.expiresAt && Date.now() < tokens.expiresAt - 60_000) {
75
+ return tokens.accessToken;
76
+ }
77
+ if (!tokens.refreshToken)
78
+ return tokens.accessToken ?? null;
79
+ const config = readAuth0Config();
80
+ const response = await fetch(`https://${config.domain}/oauth/token`, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({
84
+ grant_type: 'refresh_token',
85
+ client_id: config.clientId,
86
+ refresh_token: tokens.refreshToken,
87
+ }),
88
+ });
89
+ if (!response.ok) {
90
+ throw new Error('Stored refresh token is no longer valid. Run artifact-studio login again.');
91
+ }
92
+ const refreshed = await response.json();
93
+ await writeTokenConfig({
94
+ accessToken: refreshed.access_token,
95
+ refreshToken: refreshed.refresh_token ?? tokens.refreshToken,
96
+ expiresAt: Date.now() + (refreshed.expires_in ?? 86400) * 1000,
97
+ });
98
+ return refreshed.access_token;
99
+ }
100
+ async function waitForCode(authUrl) {
101
+ return new Promise((resolve, reject) => {
102
+ const server = createServer((req, res) => {
103
+ const url = new URL(req.url ?? '/', REDIRECT_URI);
104
+ const code = url.searchParams.get('code');
105
+ const error = url.searchParams.get('error');
106
+ if (error) {
107
+ res.writeHead(400, { 'content-type': 'text/plain' });
108
+ res.end(`Login failed: ${error}`);
109
+ server.close();
110
+ reject(new Error(`Auth0 login failed: ${error}`));
111
+ return;
112
+ }
113
+ if (!code) {
114
+ res.writeHead(400, { 'content-type': 'text/plain' });
115
+ res.end('Missing authorization code');
116
+ return;
117
+ }
118
+ res.writeHead(200, { 'content-type': 'text/html' });
119
+ res.end('<h1>Artifact Studio login complete</h1><p>You may close this tab.</p>');
120
+ server.close();
121
+ resolve(code);
122
+ });
123
+ server.once('error', reject);
124
+ server.listen(REDIRECT_PORT, () => {
125
+ console.log(`Opening browser for Auth0 login. If it does not open, visit:\n${authUrl}`);
126
+ openBrowser(authUrl);
127
+ });
128
+ });
129
+ }
@@ -0,0 +1,12 @@
1
+ import { type ArtifactStudioSourceFile } from './project.js';
2
+ import type { ArtifactStudioManifest } from './manifest.js';
3
+ export interface ArtifactStudioBuildResult {
4
+ manifest: ArtifactStudioManifest;
5
+ files: ArtifactStudioSourceFile[];
6
+ bundle: string;
7
+ sourceMap: string | null;
8
+ sourceHash: string;
9
+ bundleHash: string;
10
+ }
11
+ export declare function buildArtifactStudioProject(rootDir: string): Promise<ArtifactStudioBuildResult>;
12
+ export declare function writeBuildArtifact(result: ArtifactStudioBuildResult, outPath: string): Promise<void>;
package/dist/build.js ADDED
@@ -0,0 +1,134 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { build as viteBuild } from 'vite';
6
+ import { sha256Hex } from './hash.js';
7
+ import { readArtifactStudioSource } from './project.js';
8
+ const cliRequire = createRequire(import.meta.url);
9
+ const cliDir = dirname(fileURLToPath(import.meta.url));
10
+ const reactDir = dirname(cliRequire.resolve('react/package.json'));
11
+ const reactDomDir = dirname(cliRequire.resolve('react-dom/package.json'));
12
+ const atlasUiDir = dirname(cliRequire.resolve('@sequenceholdings/atlas-ui/package.json'));
13
+ const atlasUiDist = resolve(atlasUiDir, 'dist');
14
+ const tailwindCssPath = resolve(dirname(cliRequire.resolve('tailwindcss/package.json')), 'index.css');
15
+ const twAnimateCssPath = resolve(cliDir, '..', 'node_modules', 'tw-animate-css', 'dist', 'tw-animate.css');
16
+ function isAssetOutput(value) {
17
+ return value.type === 'asset';
18
+ }
19
+ export async function buildArtifactStudioProject(rootDir) {
20
+ const source = await readArtifactStudioSource(rootDir);
21
+ const root = source.root;
22
+ const result = await viteBuild({
23
+ root,
24
+ configFile: false,
25
+ logLevel: 'silent',
26
+ plugins: [
27
+ artifactSdkVirtualModule(),
28
+ atlasUiCssResolverPlugin(),
29
+ (await import('@tailwindcss/vite')).default(),
30
+ ],
31
+ resolve: {
32
+ alias: {
33
+ react: reactDir,
34
+ 'react-dom/client': resolve(reactDomDir, 'client'),
35
+ 'react-dom': reactDomDir,
36
+ '@sequenceholdings/atlas-ui/tokens.css': resolve(atlasUiDist, 'tokens.css'),
37
+ '@sequenceholdings/atlas-ui/charts': resolve(atlasUiDist, 'charts.mjs'),
38
+ '@sequenceholdings/atlas-ui': resolve(atlasUiDist, 'index.mjs'),
39
+ },
40
+ },
41
+ build: {
42
+ write: false,
43
+ sourcemap: true,
44
+ cssCodeSplit: false,
45
+ assetsInlineLimit: Number.MAX_SAFE_INTEGER,
46
+ rollupOptions: {
47
+ input: resolve(root, 'index.html'),
48
+ output: {
49
+ format: 'iife',
50
+ inlineDynamicImports: true,
51
+ name: 'ArtifactStudioApp',
52
+ },
53
+ },
54
+ },
55
+ });
56
+ const output = Array.isArray(result) ? result.flatMap((r) => r.output) : result.output;
57
+ const js = collectEntryJavaScript(output);
58
+ if (!js)
59
+ throw new Error('Build produced no JavaScript output');
60
+ const css = collectCss(output);
61
+ const bundle = css ? injectCssRuntime(css) + '\n' + js : js;
62
+ return {
63
+ manifest: source.manifest,
64
+ files: source.files,
65
+ sourceHash: source.sourceHash,
66
+ bundle,
67
+ sourceMap: collectSourceMap(output),
68
+ bundleHash: sha256Hex(bundle),
69
+ };
70
+ }
71
+ export async function writeBuildArtifact(result, outPath) {
72
+ const target = resolve(outPath);
73
+ await mkdir(dirname(target), { recursive: true });
74
+ await writeFile(target, JSON.stringify(result, null, 2) + '\n', 'utf8');
75
+ }
76
+ function artifactSdkVirtualModule() {
77
+ const virtualIds = new Set(['@sequence/artifact-sdk', '@sequenceholdings/artifact-studio']);
78
+ const resolved = '\0sequence-artifact-sdk';
79
+ return {
80
+ name: 'sequence-artifact-sdk',
81
+ resolveId(id) {
82
+ return virtualIds.has(id) ? resolved : undefined;
83
+ },
84
+ load(id) {
85
+ if (id !== resolved)
86
+ return undefined;
87
+ return 'export const seq = window.seq; export default window.seq;';
88
+ },
89
+ };
90
+ }
91
+ function collectEntryJavaScript(output) {
92
+ const entryChunks = output.filter((chunk) => chunk.type === 'chunk' && chunk.isEntry);
93
+ if (entryChunks.length === 0)
94
+ return null;
95
+ return entryChunks.map((chunk) => chunk.code).join('\n');
96
+ }
97
+ function collectCss(output) {
98
+ const cssAssets = output.filter((chunk) => isAssetOutput(chunk) && chunk.fileName.endsWith('.css'));
99
+ if (cssAssets.length === 0)
100
+ return null;
101
+ return cssAssets.map((asset) => String(asset.source)).join('\n');
102
+ }
103
+ function injectCssRuntime(css) {
104
+ return `(() => {
105
+ const style = document.createElement('style');
106
+ style.textContent = ${JSON.stringify(css)};
107
+ document.head.appendChild(style);
108
+ })();`;
109
+ }
110
+ function collectSourceMap(output) {
111
+ const maps = output
112
+ .filter((chunk) => chunk.type === 'chunk' && !!chunk.map)
113
+ .map((chunk) => ({ fileName: chunk.fileName, map: chunk.map }));
114
+ return maps.length > 0 ? JSON.stringify(maps) : null;
115
+ }
116
+ function atlasUiCssResolverPlugin() {
117
+ return {
118
+ name: 'atlas-ui-css-resolver',
119
+ enforce: 'pre',
120
+ transform(code, id) {
121
+ if (!id.endsWith('.css'))
122
+ return undefined;
123
+ let result = code;
124
+ if (result.includes('@import "tailwindcss"')) {
125
+ result = result.replace('@import "tailwindcss"', `@import ${JSON.stringify(tailwindCssPath)}`);
126
+ result = `@source ${JSON.stringify(atlasUiDist)};\n${result}`;
127
+ }
128
+ if (result.includes('@import "tw-animate-css"')) {
129
+ result = result.replace('@import "tw-animate-css"', `@import ${JSON.stringify(twAnimateCssPath)}`);
130
+ }
131
+ return result !== code ? result : undefined;
132
+ },
133
+ };
134
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function runCli(argv?: string[]): Promise<number>;