@sequenceholdings/artifact-studio 0.1.6 → 0.1.9

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 (49) hide show
  1. package/dist/api.js +66 -9
  2. package/dist/auth.js +1 -1
  3. package/dist/build-subprocess.d.ts +15 -0
  4. package/dist/build-subprocess.js +65 -0
  5. package/dist/build-worker.d.ts +1 -0
  6. package/dist/build-worker.js +28 -0
  7. package/dist/build.d.ts +11 -0
  8. package/dist/build.js +85 -14
  9. package/dist/cli.d.ts +1 -0
  10. package/dist/cli.js +486 -118
  11. package/dist/config.d.ts +1 -0
  12. package/dist/config.js +4 -1
  13. package/dist/dev-link.d.ts +60 -0
  14. package/dist/dev-link.js +155 -0
  15. package/dist/dev-lock.d.ts +5 -0
  16. package/dist/dev-lock.js +83 -0
  17. package/dist/git-clone.d.ts +41 -0
  18. package/dist/git-clone.js +178 -0
  19. package/dist/git-service-client.d.ts +32 -0
  20. package/dist/git-service-client.js +122 -0
  21. package/dist/hash.d.ts +12 -0
  22. package/dist/hash.js +13 -1
  23. package/dist/lockfile-origin.d.ts +6 -0
  24. package/dist/lockfile-origin.js +41 -0
  25. package/dist/manifest-custom-roles.d.ts +63 -0
  26. package/dist/manifest-custom-roles.js +194 -0
  27. package/dist/manifest.d.ts +31 -0
  28. package/dist/manifest.js +77 -0
  29. package/dist/prepare-build.d.ts +21 -0
  30. package/dist/prepare-build.js +94 -0
  31. package/dist/project.js +2 -1
  32. package/dist/sandbox-lint.d.ts +21 -0
  33. package/dist/sandbox-lint.js +95 -0
  34. package/dist/sanitize-remote-tree.d.ts +19 -0
  35. package/dist/sanitize-remote-tree.js +52 -0
  36. package/dist/sdk.d.ts +49 -0
  37. package/dist/source-resolver.d.ts +68 -0
  38. package/dist/source-resolver.js +202 -0
  39. package/dist/templates/react-vite/CLAUDE.md +18 -7
  40. package/dist/templates/react-vite/package.json +8 -7
  41. package/dist/trusted-install.d.ts +13 -0
  42. package/dist/trusted-install.js +186 -0
  43. package/dist/watch-loop.d.ts +26 -0
  44. package/dist/watch-loop.js +31 -0
  45. package/package.json +29 -9
  46. package/templates/react-vite/CLAUDE.md +18 -7
  47. package/templates/react-vite/package.json +8 -7
  48. package/dist/bin.d.ts +0 -2
  49. package/dist/bin.js +0 -8
@@ -13,25 +13,26 @@ This is a sandboxed iframe SPA that runs inside Atlas and talks to Atlas backend
13
13
 
14
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
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`:
16
+ 3. **Capability matcher: exact paths, `/*` prefix wildcards, and single-segment `*` patterns.** `{id}` template forms do **not** match. List both the bare path and the prefix wildcard for broad resources, or use a least-privilege segment pattern for dynamic routes:
17
17
  ```yaml
18
18
  - /api/crm/customers
19
19
  - /api/crm/customers/*
20
+ - /api/lattice/runs/*/advance
20
21
  ```
21
22
 
22
23
  ## Prerequisite
23
24
 
24
- `artifact-studio init` requires the Sequence monorepo checked out locally. The CLI rewrites `"@sequenceholdings/atlas-ui": "workspace:*"` and the `@sequenceholdings/artifact-studio` version in this `package.json` to local `file:` paths against the monorepo's `shared/services/atlas-ui` and `shared/services/artifact-studio`. Without the monorepo the rewrite no-ops, and `npm install` fails with `EUNSUPPORTEDPROTOCOL` on `workspace:*` those two packages aren't published to npm. Run init from inside the monorepo, e.g. `pnpm --dir atlas artifact-studio init <dir>`.
25
+ None — `seq-studio artifact init <dir>` works from any directory; no monorepo checkout is required. The template lists `@sequenceholdings/artifact-studio` and `@sequenceholdings/atlas-ui` in **devDependencies** for local editor/type-check tooling only. `seq-studio artifact build|deploy` bundles against the CLI's own pinned copies of `react`, `react-dom`, `@tanstack/react-query`, `react-router-dom`, `sonner`, and `@sequenceholdings/atlas-ui`, and resolves `@sequenceholdings/artifact-studio` to a virtual `window.seq` module. Remote deploys (`--repo` / `--git-url`) run `pnpm install --prod` for **app-specific** `dependencies` only (e.g. `recharts`, `reactflow`) from the artifact lockfile.
25
26
 
26
- ## Package manager: npm, not pnpm
27
+ ## Package manager
27
28
 
28
- Once the CLI has rewritten the deps, use `npm install`, not `pnpm install`. The linked `@sequenceholdings/artifact-studio` package carries `"@sequenceholdings/atlas-ui": "workspace:*"` in its own `package.json`; npm tolerates an unresolved `workspace:*` inside a file-linked dep, pnpm errors with `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`. If a `pnpm-lock.yaml` already exists, `rm -rf node_modules pnpm-lock.yaml` before `npm install`.
29
+ Use **pnpm** for artifacts you will deploy from git `seq-studio artifact build|deploy --repo/--git-url` runs `pnpm install --frozen-lockfile --prod` when `package.json` is present, so commit `pnpm-lock.yaml`. Local editor-only installs can use another package manager, but pnpm keeps Sequence's `minimumReleaseAge` supply-chain quarantine in effect (see the studio-cli README). If you have a stale checkout from the old `file:`-link era, `rm -rf node_modules` and reinstall.
29
30
 
30
31
  ## Key files
31
32
 
32
33
  | File | Role |
33
34
  |---|---|
34
- | `artifact.bundle.yml` | Capability manifest — every API path the artifact calls, exact + `/*` |
35
+ | `artifact.bundle.yml` | Capability manifest — every API path the artifact calls: exact paths, `/*` prefix wildcards, or single-segment `*` patterns |
35
36
  | `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
36
37
  | `src/App.tsx` | Entrypoint UI |
37
38
  | `src/lib/api.ts` | `unwrap<T>` envelope helper + re-export of `seq` |
@@ -40,10 +41,10 @@ Once the CLI has rewritten the deps, use `npm install`, not `pnpm install`. The
40
41
 
41
42
  ## Defaults
42
43
 
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
+ - UI: prefer `@sequenceholdings/atlas-ui` primitives (`Button`, `Card`, `Input`, `Dialog`, `Select`, `Checkbox`, `DropdownMenu`, `KpiTile`, `Switch`, …) over raw Tailwind. The design-system cutover made `Select`, `Checkbox`, `DropdownMenu`, `Command`, `Calendar`, `DatePicker`, `ScrollArea`, `Table`, and the Task family first-class exports — use them directly; fall back to `Popover` compositions or native controls only when a component is genuinely missing.
44
45
  - Routing: `HashRouter`. Mount routes at `/`, not at the Atlas app prefix.
45
46
  - 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
+ - Icons: do **not** add third-party deps like `@tabler/icons-react`. Deploys from git (`--repo` / `--git-url`) build a bare tree `seq-studio` runs `pnpm install --prod` from your lockfile, then aliases platform peers (`react`, `react-dom`, `@tanstack/react-query`, `react-router-dom`, `sonner`, `@sequenceholdings/atlas-ui`). List only app-specific imports (e.g. `recharts`) in `dependencies`; do not add file/workspace `@sequenceholdings/*` specs in git-service repos, because remote installs only accept registry semver specs. Inline SVG components in `src/icons.tsx`.
47
48
 
48
49
  ## Bridge methods
49
50
 
@@ -53,3 +54,13 @@ Two primitives on `seq.api`:
53
54
  - `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
 
55
56
  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.
57
+
58
+ ## Lattice embed context
59
+
60
+ When the artifact is served from a Lattice human node (the **Tasks/Inbox** task surface, or the builder Artifacts tab), `seq.lattice.context()` tells you which run/node/step you're rendering for:
61
+
62
+ ```ts
63
+ const { runId, nodeId, stepId, processId } = seq.lattice.context()
64
+ ```
65
+
66
+ It's **synchronous** (the host injects this into the sandbox up front — no bridge round trip) and each field is `null` when unavailable. The Inbox provides all four; the builder Artifacts tab has no live `stepId`; a standalone open has no context at all. Use it instead of hand-parsing `window.location` — that's the sanctioned, stable accessor, and you don't need to declare anything in `artifact.bundle.yml` for it. To read step-scoped data, pass `runId`/`stepId` to the relevant Atlas endpoint (which still authorizes the caller server-side).
@@ -7,20 +7,21 @@
7
7
  "build": "vite build",
8
8
  "preview": "vite preview"
9
9
  },
10
- "dependencies": {
10
+ "dependencies": {},
11
+ "devDependencies": {
11
12
  "@sequenceholdings/artifact-studio": "^0.1.0",
12
13
  "@sequenceholdings/atlas-ui": "^0.1.0",
13
- "@tanstack/react-query": "^4.36.1",
14
- "react": "^18.2.0",
15
- "react-dom": "^18.2.0"
16
- },
17
- "devDependencies": {
18
14
  "@tailwindcss/postcss": "^4.2.2",
19
15
  "@tailwindcss/vite": "^4.2.2",
16
+ "@tanstack/react-query": "^4.36.1",
20
17
  "@vitejs/plugin-react": "^5.0.0",
18
+ "react": "^18.2.0",
19
+ "react-dom": "^18.2.0",
20
+ "react-router-dom": "^6.28.0",
21
+ "sonner": "^2.0.7",
21
22
  "tailwindcss": "^4.2.2",
22
23
  "tw-animate-css": "^1.4.0",
23
24
  "typescript": "^5.6.0",
24
- "vite": "8.0.8"
25
+ "vite": "8.0.16"
25
26
  }
26
27
  }
@@ -0,0 +1,13 @@
1
+ export declare class ArtifactInstallTrustError extends Error {
2
+ readonly name = "ArtifactInstallTrustError";
3
+ }
4
+ /** Dot optional — matches both `pnpmfile.cjs` and `.pnpmfile.cjs`. */
5
+ export declare const PNPM_HOOK_FILE_RE: RegExp;
6
+ export declare const PNPM_WORKSPACE_FILE_RE: RegExp;
7
+ /** Platform peers the CLI aliases at build time — must not be prod-installed remotely. */
8
+ export declare const REMOTE_INSTALL_PLATFORM_PACKAGES: Set<string>;
9
+ /**
10
+ * Reject install-control files and non-registry dependency specs before a remote
11
+ * `pnpm install` runs on an untrusted git checkout.
12
+ */
13
+ export declare function assertTrustedArtifactInstallTree(dir: string): void;
@@ -0,0 +1,186 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { classifyLockfileOrigin } from './lockfile-origin.js';
4
+ export class ArtifactInstallTrustError extends Error {
5
+ name = 'ArtifactInstallTrustError';
6
+ }
7
+ /** Dot optional — matches both `pnpmfile.cjs` and `.pnpmfile.cjs`. */
8
+ export const PNPM_HOOK_FILE_RE = /(^|\/)\.?pnpmfile\.cjs$/;
9
+ export const PNPM_WORKSPACE_FILE_RE = /(^|\/)pnpm-workspace\.ya?ml$/;
10
+ const NON_REGISTRY_SPEC_RE = /^(git\+|git:|github:|gitlab:|bitbucket:|https?:|file:|link:|workspace:|catalog:|\.{0,2}\/)/i;
11
+ const DEPENDENCY_SECTIONS = [
12
+ 'dependencies',
13
+ 'devDependencies',
14
+ 'optionalDependencies',
15
+ 'peerDependencies',
16
+ ];
17
+ const LOCKFILE_FORBIDDEN_RE = /(^|[\s{,])(repo|commit|directory):\s/m;
18
+ const LOCKFILE_TARBALL_RE = /(^|[\s{,])tarball:\s*([^\n,}]+)/g;
19
+ const ALLOWED_TARBALL_PREFIXES = [
20
+ 'https://libraries.cgr.dev/javascript/',
21
+ 'https://libraries.cgr.dev/javascript-upstream/',
22
+ ];
23
+ const EXCLUDED_DIRS = new Set(['node_modules', '.git']);
24
+ /** Platform peers the CLI aliases at build time — must not be prod-installed remotely. */
25
+ export const REMOTE_INSTALL_PLATFORM_PACKAGES = new Set([
26
+ 'react',
27
+ 'react-dom',
28
+ '@tanstack/react-query',
29
+ 'react-router-dom',
30
+ 'sonner',
31
+ '@sequenceholdings/atlas-ui',
32
+ '@sequenceholdings/lattice-form-renderer',
33
+ '@sequenceholdings/artifact-studio',
34
+ ]);
35
+ function listRelativeFiles(dir, prefix = '') {
36
+ const paths = [];
37
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
38
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
39
+ if (entry.isSymbolicLink()) {
40
+ throw new ArtifactInstallTrustError(`Symlinks are not allowed in remote artifact sources (${rel})`);
41
+ }
42
+ if (entry.isDirectory()) {
43
+ if (EXCLUDED_DIRS.has(entry.name))
44
+ continue;
45
+ paths.push(...listRelativeFiles(join(dir, entry.name), rel));
46
+ continue;
47
+ }
48
+ if (entry.isFile())
49
+ paths.push(rel);
50
+ }
51
+ return paths;
52
+ }
53
+ function isNonRegistrySpec(spec) {
54
+ if (typeof spec !== 'string')
55
+ return false;
56
+ if (spec.startsWith('$'))
57
+ return false;
58
+ if (spec.startsWith('npm:')) {
59
+ const target = spec.slice(4);
60
+ return NON_REGISTRY_SPEC_RE.test(target) || target.includes('/');
61
+ }
62
+ return NON_REGISTRY_SPEC_RE.test(spec) || spec.includes('/');
63
+ }
64
+ function assertNoPackageManagerSelection(pkg) {
65
+ if (typeof pkg.packageManager === 'string' && pkg.packageManager.trim() !== '') {
66
+ throw new ArtifactInstallTrustError('package.json sets packageManager — remote artifact installs use the seq-studio pinned pnpm binary');
67
+ }
68
+ const devEngines = pkg.devEngines;
69
+ if (devEngines && typeof devEngines === 'object' && 'packageManager' in devEngines) {
70
+ const pm = devEngines.packageManager;
71
+ if (pm !== undefined && pm !== null) {
72
+ throw new ArtifactInstallTrustError('package.json sets devEngines.packageManager — remote artifact installs use the seq-studio pinned pnpm binary');
73
+ }
74
+ }
75
+ }
76
+ function assertRegistryOnlySpecMap(map, label) {
77
+ if (!map || typeof map !== 'object')
78
+ return;
79
+ for (const [name, spec] of Object.entries(map)) {
80
+ if (isNonRegistrySpec(spec)) {
81
+ throw new ArtifactInstallTrustError(`${label} "${name}" uses a non-registry spec ("${String(spec)}") — only registry semver ranges are allowed`);
82
+ }
83
+ }
84
+ }
85
+ /** Sections installed by `pnpm install --prod` (devDependencies are omitted). */
86
+ const PROD_INSTALL_SECTIONS = ['dependencies', 'optionalDependencies'];
87
+ function assertNoPlatformPackagesInProdSections(dir) {
88
+ const raw = readFileSync(join(dir, 'package.json'), 'utf8');
89
+ let pkg;
90
+ try {
91
+ pkg = JSON.parse(raw);
92
+ }
93
+ catch {
94
+ throw new ArtifactInstallTrustError('package.json is not valid JSON');
95
+ }
96
+ for (const section of PROD_INSTALL_SECTIONS) {
97
+ const deps = pkg[section];
98
+ if (!deps || typeof deps !== 'object')
99
+ continue;
100
+ for (const name of Object.keys(deps)) {
101
+ if (REMOTE_INSTALL_PLATFORM_PACKAGES.has(name)) {
102
+ throw new ArtifactInstallTrustError(`Dependency "${name}" is a platform peer aliased by seq-studio at build time — move it to devDependencies (local editor tooling only) and keep only app-specific packages in dependencies for git-service deploys`);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ function assertRegistryOnlyPackageJson(dir) {
108
+ const raw = readFileSync(join(dir, 'package.json'), 'utf8');
109
+ let pkg;
110
+ try {
111
+ pkg = JSON.parse(raw);
112
+ }
113
+ catch {
114
+ throw new ArtifactInstallTrustError('package.json is not valid JSON');
115
+ }
116
+ for (const section of DEPENDENCY_SECTIONS) {
117
+ assertRegistryOnlySpecMap(pkg[section], 'Dependency');
118
+ }
119
+ assertRegistryOnlySpecMap(pkg.resolutions, 'Resolution override');
120
+ assertNoPackageManagerSelection(pkg);
121
+ const pnpmConfig = pkg.pnpm;
122
+ if (pnpmConfig && typeof pnpmConfig === 'object') {
123
+ const cfg = pnpmConfig;
124
+ const patched = cfg.patchedDependencies;
125
+ if (patched && typeof patched === 'object' && Object.keys(patched).length > 0) {
126
+ throw new ArtifactInstallTrustError('package.json sets pnpm.patchedDependencies — dependency patches are not allowed in remote artifact installs');
127
+ }
128
+ assertRegistryOnlySpecMap(cfg.overrides, 'pnpm override');
129
+ const exts = cfg.packageExtensions;
130
+ if (exts && typeof exts === 'object') {
131
+ for (const [extName, ext] of Object.entries(exts)) {
132
+ if (!ext || typeof ext !== 'object')
133
+ continue;
134
+ const extObj = ext;
135
+ for (const section of DEPENDENCY_SECTIONS) {
136
+ assertRegistryOnlySpecMap(extObj[section], `pnpm packageExtensions "${extName}" ${section}`);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+ function normalizeYamlScalar(raw) {
143
+ const trimmed = raw.trim();
144
+ if (trimmed.length >= 2 &&
145
+ (trimmed[0] === '"' || trimmed[0] === "'") &&
146
+ trimmed[trimmed.length - 1] === trimmed[0]) {
147
+ return trimmed.slice(1, -1);
148
+ }
149
+ return trimmed;
150
+ }
151
+ function assertChainguardLockfile(dir) {
152
+ const lockfileText = readFileSync(join(dir, 'pnpm-lock.yaml'), 'utf8');
153
+ if (classifyLockfileOrigin(lockfileText) !== 'chainguard') {
154
+ throw new ArtifactInstallTrustError('pnpm-lock.yaml must be resolved against Chainguard (registry=https://libraries.cgr.dev/javascript/) — regenerate locally and commit the lockfile');
155
+ }
156
+ const forbidden = LOCKFILE_FORBIDDEN_RE.exec(lockfileText);
157
+ if (forbidden) {
158
+ throw new ArtifactInstallTrustError(`pnpm-lock.yaml resolves at least one dependency outside the registry (found "${forbidden[2]}:" resolution)`);
159
+ }
160
+ for (const match of lockfileText.matchAll(LOCKFILE_TARBALL_RE)) {
161
+ const tarball = normalizeYamlScalar(match[2] ?? '');
162
+ if (!ALLOWED_TARBALL_PREFIXES.some((prefix) => tarball.startsWith(prefix))) {
163
+ throw new ArtifactInstallTrustError(`pnpm-lock.yaml tarball resolution "${tarball}" is not from the Chainguard registry`);
164
+ }
165
+ }
166
+ }
167
+ /**
168
+ * Reject install-control files and non-registry dependency specs before a remote
169
+ * `pnpm install` runs on an untrusted git checkout.
170
+ */
171
+ export function assertTrustedArtifactInstallTree(dir) {
172
+ for (const rel of listRelativeFiles(dir)) {
173
+ if (rel === '.npmrc' || rel.endsWith('/.npmrc')) {
174
+ throw new ArtifactInstallTrustError('Source .npmrc is not allowed — seq-studio pins the registry in a controlled install config');
175
+ }
176
+ if (PNPM_HOOK_FILE_RE.test(rel)) {
177
+ throw new ArtifactInstallTrustError(`${rel} is not allowed — pnpm hook files execute during install`);
178
+ }
179
+ if (PNPM_WORKSPACE_FILE_RE.test(rel)) {
180
+ throw new ArtifactInstallTrustError(`${rel} is not allowed — pnpm-workspace.yaml can redirect dependencies outside the pinned registry`);
181
+ }
182
+ }
183
+ assertNoPlatformPackagesInProdSections(dir);
184
+ assertRegistryOnlyPackageJson(dir);
185
+ assertChainguardLockfile(dir);
186
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Non-overlapping poll loop for watch-style commands (e.g. `artifact dev`).
3
+ *
4
+ * A naive `setInterval(tick, intervalMs)` fires on a fixed cadence
5
+ * regardless of whether the previous `tick` finished. When `tick` is an
6
+ * expensive, CPU-bound rebuild that runs longer than `intervalMs`, builds
7
+ * stack up concurrently — each holding a full in-memory module graph —
8
+ * and contend for CPU/heap, which makes each build slower, which lets even
9
+ * more pile up. That positive-feedback loop is unbounded and OOMs the
10
+ * process.
11
+ *
12
+ * This scheduler guarantees a single `tick` is ever in flight: it awaits
13
+ * the current tick, then waits `intervalMs`, then repeats — so the next
14
+ * tick never starts until the previous one has fully settled.
15
+ */
16
+ export interface RunWatchLoopOptions {
17
+ /** The work to run each cycle. Awaited to completion before the next cycle is scheduled. */
18
+ tick: () => Promise<void>;
19
+ /** Idle delay between the end of one tick and the start of the next. */
20
+ intervalMs: number;
21
+ /** Aborting stops the loop: no further tick runs and the returned promise resolves. */
22
+ signal: AbortSignal;
23
+ /** Invoked when a `tick` rejects. The loop continues regardless — one failure never kills it. */
24
+ onError?: (error: unknown) => void;
25
+ }
26
+ export declare function runWatchLoop({ tick, intervalMs, signal, onError, }: RunWatchLoopOptions): Promise<void>;
@@ -0,0 +1,31 @@
1
+ export async function runWatchLoop({ tick, intervalMs, signal, onError, }) {
2
+ while (!signal.aborted) {
3
+ try {
4
+ await tick();
5
+ }
6
+ catch (error) {
7
+ onError?.(error);
8
+ }
9
+ if (signal.aborted)
10
+ break;
11
+ await delay(intervalMs, signal);
12
+ }
13
+ }
14
+ /** Resolve after `ms`, or immediately when `signal` aborts. Cleans up its timer/listener either way. */
15
+ function delay(ms, signal) {
16
+ return new Promise((resolve) => {
17
+ if (signal.aborted) {
18
+ resolve();
19
+ return;
20
+ }
21
+ const onAbort = () => {
22
+ clearTimeout(timer);
23
+ resolve();
24
+ };
25
+ const timer = setTimeout(() => {
26
+ signal.removeEventListener('abort', onAbort);
27
+ resolve();
28
+ }, ms);
29
+ signal.addEventListener('abort', onAbort, { once: true });
30
+ });
31
+ }
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@sequenceholdings/artifact-studio",
3
+ "license": "UNLICENSED",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/sequenceholdings/studio.git",
7
+ "directory": "shared/services/artifact-studio"
8
+ },
3
9
  "publishConfig": {
4
- "access": "restricted"
10
+ "access": "public"
5
11
  },
6
- "version": "0.1.6",
7
- "description": "CLI and SDK types for building Artifact Studio apps",
12
+ "version": "0.1.9",
13
+ "description": "SDK types and CLI library for building Artifact Studio apps. Driven via `seq-studio artifact <sub>` (@sequenceholdings/studio-cli), which runs the `./cli` runCli export.",
8
14
  "type": "module",
9
- "bin": {
10
- "artifact-studio": "./dist/bin.js"
11
- },
12
15
  "exports": {
13
16
  ".": {
14
17
  "types": "./dist/sdk.d.ts",
@@ -17,6 +20,18 @@
17
20
  "./cli": {
18
21
  "types": "./dist/cli.d.ts",
19
22
  "default": "./dist/cli.js"
23
+ },
24
+ "./source-resolver": {
25
+ "types": "./dist/source-resolver.d.ts",
26
+ "default": "./dist/source-resolver.js"
27
+ },
28
+ "./git-service-client": {
29
+ "types": "./dist/git-service-client.d.ts",
30
+ "default": "./dist/git-service-client.js"
31
+ },
32
+ "./git-clone": {
33
+ "types": "./dist/git-clone.d.ts",
34
+ "default": "./dist/git-clone.js"
20
35
  }
21
36
  },
22
37
  "files": [
@@ -26,14 +41,18 @@
26
41
  "dependencies": {
27
42
  "@tailwindcss/postcss": "^4.1.8",
28
43
  "@tailwindcss/vite": "^4.1.8",
44
+ "@tanstack/react-query": "^4.36.1",
29
45
  "js-yaml": "^4.1.1",
30
46
  "react": "^18.2.0",
31
47
  "react-dom": "^18.2.0",
48
+ "react-router-dom": "^6.30.4",
49
+ "sonner": "^2.0.7",
32
50
  "tailwindcss": "^4.1.8",
33
51
  "tw-animate-css": "^1.4.0",
34
- "vite": "8.0.8",
52
+ "vite": "8.0.16",
35
53
  "zod": "^4.1.13",
36
- "@sequenceholdings/atlas-ui": "0.1.2"
54
+ "@sequenceholdings/atlas-ui": "0.1.3",
55
+ "@sequenceholdings/lattice-form-renderer": "0.1.0"
37
56
  },
38
57
  "devDependencies": {
39
58
  "@types/js-yaml": "^4.0.9",
@@ -44,6 +63,7 @@
44
63
  "scripts": {
45
64
  "build": "tsc && node scripts/copy-templates.mjs",
46
65
  "type-check": "tsc --noEmit",
47
- "test": "vitest run"
66
+ "test": "vitest run",
67
+ "bench:build-isolation": "node --expose-gc scripts/bench-build-isolation.mjs"
48
68
  }
49
69
  }
@@ -13,25 +13,26 @@ This is a sandboxed iframe SPA that runs inside Atlas and talks to Atlas backend
13
13
 
14
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
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`:
16
+ 3. **Capability matcher: exact paths, `/*` prefix wildcards, and single-segment `*` patterns.** `{id}` template forms do **not** match. List both the bare path and the prefix wildcard for broad resources, or use a least-privilege segment pattern for dynamic routes:
17
17
  ```yaml
18
18
  - /api/crm/customers
19
19
  - /api/crm/customers/*
20
+ - /api/lattice/runs/*/advance
20
21
  ```
21
22
 
22
23
  ## Prerequisite
23
24
 
24
- `artifact-studio init` requires the Sequence monorepo checked out locally. The CLI rewrites `"@sequenceholdings/atlas-ui": "workspace:*"` and the `@sequenceholdings/artifact-studio` version in this `package.json` to local `file:` paths against the monorepo's `shared/services/atlas-ui` and `shared/services/artifact-studio`. Without the monorepo the rewrite no-ops, and `npm install` fails with `EUNSUPPORTEDPROTOCOL` on `workspace:*` those two packages aren't published to npm. Run init from inside the monorepo, e.g. `pnpm --dir atlas artifact-studio init <dir>`.
25
+ None — `seq-studio artifact init <dir>` works from any directory; no monorepo checkout is required. The template lists `@sequenceholdings/artifact-studio` and `@sequenceholdings/atlas-ui` in **devDependencies** for local editor/type-check tooling only. `seq-studio artifact build|deploy` bundles against the CLI's own pinned copies of `react`, `react-dom`, `@tanstack/react-query`, `react-router-dom`, `sonner`, and `@sequenceholdings/atlas-ui`, and resolves `@sequenceholdings/artifact-studio` to a virtual `window.seq` module. Remote deploys (`--repo` / `--git-url`) run `pnpm install --prod` for **app-specific** `dependencies` only (e.g. `recharts`, `reactflow`) from the artifact lockfile.
25
26
 
26
- ## Package manager: npm, not pnpm
27
+ ## Package manager
27
28
 
28
- Once the CLI has rewritten the deps, use `npm install`, not `pnpm install`. The linked `@sequenceholdings/artifact-studio` package carries `"@sequenceholdings/atlas-ui": "workspace:*"` in its own `package.json`; npm tolerates an unresolved `workspace:*` inside a file-linked dep, pnpm errors with `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`. If a `pnpm-lock.yaml` already exists, `rm -rf node_modules pnpm-lock.yaml` before `npm install`.
29
+ Use **pnpm** for artifacts you will deploy from git `seq-studio artifact build|deploy --repo/--git-url` runs `pnpm install --frozen-lockfile --prod` when `package.json` is present, so commit `pnpm-lock.yaml`. Local editor-only installs can use another package manager, but pnpm keeps Sequence's `minimumReleaseAge` supply-chain quarantine in effect (see the studio-cli README). If you have a stale checkout from the old `file:`-link era, `rm -rf node_modules` and reinstall.
29
30
 
30
31
  ## Key files
31
32
 
32
33
  | File | Role |
33
34
  |---|---|
34
- | `artifact.bundle.yml` | Capability manifest — every API path the artifact calls, exact + `/*` |
35
+ | `artifact.bundle.yml` | Capability manifest — every API path the artifact calls: exact paths, `/*` prefix wildcards, or single-segment `*` patterns |
35
36
  | `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
36
37
  | `src/App.tsx` | Entrypoint UI |
37
38
  | `src/lib/api.ts` | `unwrap<T>` envelope helper + re-export of `seq` |
@@ -40,10 +41,10 @@ Once the CLI has rewritten the deps, use `npm install`, not `pnpm install`. The
40
41
 
41
42
  ## Defaults
42
43
 
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
+ - UI: prefer `@sequenceholdings/atlas-ui` primitives (`Button`, `Card`, `Input`, `Dialog`, `Select`, `Checkbox`, `DropdownMenu`, `KpiTile`, `Switch`, …) over raw Tailwind. The design-system cutover made `Select`, `Checkbox`, `DropdownMenu`, `Command`, `Calendar`, `DatePicker`, `ScrollArea`, `Table`, and the Task family first-class exports — use them directly; fall back to `Popover` compositions or native controls only when a component is genuinely missing.
44
45
  - Routing: `HashRouter`. Mount routes at `/`, not at the Atlas app prefix.
45
46
  - 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
+ - Icons: do **not** add third-party deps like `@tabler/icons-react`. Deploys from git (`--repo` / `--git-url`) build a bare tree `seq-studio` runs `pnpm install --prod` from your lockfile, then aliases platform peers (`react`, `react-dom`, `@tanstack/react-query`, `react-router-dom`, `sonner`, `@sequenceholdings/atlas-ui`). List only app-specific imports (e.g. `recharts`) in `dependencies`; do not add file/workspace `@sequenceholdings/*` specs in git-service repos, because remote installs only accept registry semver specs. Inline SVG components in `src/icons.tsx`.
47
48
 
48
49
  ## Bridge methods
49
50
 
@@ -53,3 +54,13 @@ Two primitives on `seq.api`:
53
54
  - `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
 
55
56
  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.
57
+
58
+ ## Lattice embed context
59
+
60
+ When the artifact is served from a Lattice human node (the **Tasks/Inbox** task surface, or the builder Artifacts tab), `seq.lattice.context()` tells you which run/node/step you're rendering for:
61
+
62
+ ```ts
63
+ const { runId, nodeId, stepId, processId } = seq.lattice.context()
64
+ ```
65
+
66
+ It's **synchronous** (the host injects this into the sandbox up front — no bridge round trip) and each field is `null` when unavailable. The Inbox provides all four; the builder Artifacts tab has no live `stepId`; a standalone open has no context at all. Use it instead of hand-parsing `window.location` — that's the sanctioned, stable accessor, and you don't need to declare anything in `artifact.bundle.yml` for it. To read step-scoped data, pass `runId`/`stepId` to the relevant Atlas endpoint (which still authorizes the caller server-side).
@@ -7,20 +7,21 @@
7
7
  "build": "vite build",
8
8
  "preview": "vite preview"
9
9
  },
10
- "dependencies": {
10
+ "dependencies": {},
11
+ "devDependencies": {
11
12
  "@sequenceholdings/artifact-studio": "^0.1.0",
12
13
  "@sequenceholdings/atlas-ui": "^0.1.0",
13
- "@tanstack/react-query": "^4.36.1",
14
- "react": "^18.2.0",
15
- "react-dom": "^18.2.0"
16
- },
17
- "devDependencies": {
18
14
  "@tailwindcss/postcss": "^4.2.2",
19
15
  "@tailwindcss/vite": "^4.2.2",
16
+ "@tanstack/react-query": "^4.36.1",
20
17
  "@vitejs/plugin-react": "^5.0.0",
18
+ "react": "^18.2.0",
19
+ "react-dom": "^18.2.0",
20
+ "react-router-dom": "^6.28.0",
21
+ "sonner": "^2.0.7",
21
22
  "tailwindcss": "^4.2.2",
22
23
  "tw-animate-css": "^1.4.0",
23
24
  "typescript": "^5.6.0",
24
- "vite": "8.0.8"
25
+ "vite": "8.0.16"
25
26
  }
26
27
  }
package/dist/bin.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/bin.js DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runCli } from './cli.js';
3
- runCli()
4
- .then((code) => process.exit(code))
5
- .catch((error) => {
6
- console.error(error instanceof Error ? error.message : error);
7
- process.exit(1);
8
- });