@sequenceholdings/artifact-studio 0.1.12 → 0.2.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/VERSION-PIN.md +54 -0
- package/dist/active-deploy-policy.d.ts +36 -0
- package/dist/active-deploy-policy.js +66 -0
- package/dist/api.js +59 -13
- package/dist/artifact-ignore.d.ts +16 -0
- package/dist/artifact-ignore.js +38 -0
- package/dist/atlas-ui-css-plugin.d.ts +15 -0
- package/dist/atlas-ui-css-plugin.js +69 -0
- package/dist/build-subprocess.d.ts +3 -1
- package/dist/build-subprocess.js +28 -6
- package/dist/build.d.ts +25 -0
- package/dist/build.js +97 -50
- package/dist/child-environment.d.ts +5 -0
- package/dist/child-environment.js +83 -0
- package/dist/cli.d.ts +40 -1
- package/dist/cli.js +173 -87
- package/dist/config.d.ts +0 -9
- package/dist/config.js +0 -18
- package/dist/deployment-validation.d.ts +9 -0
- package/dist/deployment-validation.js +70 -0
- package/dist/git-clone.js +16 -10
- package/dist/peer-versions.d.ts +19 -0
- package/dist/peer-versions.js +44 -0
- package/dist/prepare-build.d.ts +2 -0
- package/dist/prepare-build.js +27 -7
- package/dist/project.d.ts +3 -0
- package/dist/project.js +22 -6
- package/dist/runtime-chunks.d.ts +4 -0
- package/dist/runtime-chunks.js +15 -0
- package/dist/sdk.d.ts +10 -0
- package/dist/source-resolver.d.ts +8 -5
- package/dist/source-resolver.js +81 -32
- package/dist/templates/react-vite/AGENTS.md +90 -0
- package/dist/templates/react-vite/CLAUDE.md +4 -63
- package/dist/templates/react-vite/src/main.tsx +7 -1
- package/dist/templates/react-vite/src/styles.css +11 -0
- package/package.json +18 -5
- package/templates/react-vite/AGENTS.md +90 -0
- package/templates/react-vite/CLAUDE.md +4 -63
- package/templates/react-vite/src/main.tsx +7 -1
- package/templates/react-vite/src/styles.css +11 -0
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +0 -129
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const cliRequire = createRequire(import.meta.url);
|
|
6
|
+
const thisModuleDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
function readPackageVersion({ packageJsonPath, expectedName }) {
|
|
8
|
+
const raw = readFileSync(packageJsonPath, 'utf8');
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
throw new Error(`Failed to parse ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof parsed.name === 'string' && parsed.name !== expectedName) {
|
|
17
|
+
throw new Error(`Expected package name ${expectedName} at ${packageJsonPath}, got ${parsed.name}`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof parsed.version !== 'string' || parsed.version.trim() === '') {
|
|
20
|
+
throw new Error(`Missing version in ${packageJsonPath}`);
|
|
21
|
+
}
|
|
22
|
+
return parsed.version.trim();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the building CLI + force-aliased atlas-ui versions from disk.
|
|
26
|
+
* Throws if either package.json is unreadable — a silent null here would
|
|
27
|
+
* undermine the observability DES-254 adds for the blast-radius scanner.
|
|
28
|
+
*/
|
|
29
|
+
export function resolveArtifactStudioPeerVersions() {
|
|
30
|
+
// Built code lives in `dist/`; package.json sits one level up. The same
|
|
31
|
+
// relative path works when vitest loads from `src/`.
|
|
32
|
+
const cliPackageJson = resolve(thisModuleDir, '..', 'package.json');
|
|
33
|
+
const atlasUiPackageJson = cliRequire.resolve('@sequenceholdings/atlas-ui/package.json');
|
|
34
|
+
return {
|
|
35
|
+
cliVersion: readPackageVersion({
|
|
36
|
+
packageJsonPath: cliPackageJson,
|
|
37
|
+
expectedName: '@sequenceholdings/artifact-studio',
|
|
38
|
+
}),
|
|
39
|
+
atlasUiVersion: readPackageVersion({
|
|
40
|
+
packageJsonPath: atlasUiPackageJson,
|
|
41
|
+
expectedName: '@sequenceholdings/atlas-ui',
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/prepare-build.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export declare function buildControlledArtifactNpmrc(userNpmrcPath?: string): st
|
|
|
16
16
|
export interface PrepareBuildOptions {
|
|
17
17
|
/** Remote sources (--repo / --git-url) install when package.json exists. */
|
|
18
18
|
remote?: boolean;
|
|
19
|
+
/** Parent environment override for deterministic security tests. */
|
|
20
|
+
sourceEnv?: NodeJS.ProcessEnv;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Whether the build should run `pnpm install` before Vite bundles the tree.
|
package/dist/prepare-build.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { createScrubbedChildEnv } from './child-environment.js';
|
|
6
7
|
import { stripInstallControlFiles } from './sanitize-remote-tree.js';
|
|
7
8
|
import { assertTrustedArtifactInstallTree } from './trusted-install.js';
|
|
8
9
|
/**
|
|
@@ -82,10 +83,25 @@ export async function prepareArtifactBuildRoot(dir, opts = {}) {
|
|
|
82
83
|
await rm(join(dir, 'node_modules'), { recursive: true, force: true });
|
|
83
84
|
const configDir = await mkdtemp(join(tmpdir(), 'artifact-pnpm-'));
|
|
84
85
|
const controlledNpmrc = join(configDir, '.npmrc');
|
|
85
|
-
const
|
|
86
|
-
|
|
86
|
+
const homeDir = join(configDir, 'home');
|
|
87
|
+
// Caches are deliberately scoped to one install. The untrusted package
|
|
88
|
+
// manager subprocess runs as the caller's uid, so a persistent cache would
|
|
89
|
+
// let one source poison dependencies consumed by a later build.
|
|
90
|
+
const npmCache = join(configDir, 'npm-cache');
|
|
91
|
+
const pnpmStore = join(configDir, 'pnpm-store');
|
|
92
|
+
const args = [
|
|
93
|
+
'install',
|
|
94
|
+
'--frozen-lockfile',
|
|
95
|
+
'--ignore-scripts',
|
|
96
|
+
'--prod',
|
|
97
|
+
'--store-dir',
|
|
98
|
+
pnpmStore,
|
|
99
|
+
];
|
|
100
|
+
const sourceEnv = opts.sourceEnv ?? process.env;
|
|
101
|
+
const userNpmrcPath = sourceEnv.npm_config_userconfig ?? join(homedir(), '.npmrc');
|
|
87
102
|
console.log(`[seq-studio] installing dependencies (frozen lockfile, prod only) via ${ARTIFACT_BUILD_PNPM}…`);
|
|
88
103
|
try {
|
|
104
|
+
await mkdir(homeDir, { recursive: true });
|
|
89
105
|
await writeFile(controlledNpmrc, buildControlledArtifactNpmrc(userNpmrcPath), { mode: 0o600 });
|
|
90
106
|
// Run a PINNED pnpm via `npx`, not the operator's ambient `pnpm`. The
|
|
91
107
|
// install must be reproducible across laptops and CI: in a bare temp clone
|
|
@@ -101,10 +117,14 @@ export async function prepareArtifactBuildRoot(dir, opts = {}) {
|
|
|
101
117
|
execFileSync('npx', ['--yes', ARTIFACT_BUILD_PNPM, '--dir', dir, ...args], {
|
|
102
118
|
cwd: configDir,
|
|
103
119
|
stdio: 'inherit',
|
|
104
|
-
env: {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
env: createScrubbedChildEnv({
|
|
121
|
+
homeDir,
|
|
122
|
+
source: sourceEnv,
|
|
123
|
+
additionalEnv: {
|
|
124
|
+
npm_config_userconfig: controlledNpmrc,
|
|
125
|
+
npm_config_cache: npmCache,
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
108
128
|
});
|
|
109
129
|
}
|
|
110
130
|
catch (error) {
|
package/dist/project.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export interface ArtifactStudioSource {
|
|
|
9
9
|
manifest: ArtifactStudioManifest;
|
|
10
10
|
files: ArtifactStudioSourceFile[];
|
|
11
11
|
sourceHash: string;
|
|
12
|
+
/** Path prefixes from `.artifactignore` — also enforced as non-importable by Vite. */
|
|
13
|
+
ignorePatterns: string[];
|
|
12
14
|
}
|
|
13
15
|
export declare function hasArtifactStudioManifest(rootDir: string): boolean;
|
|
14
16
|
export declare function readArtifactStudioSource(rootDir: string): Promise<ArtifactStudioSource>;
|
|
17
|
+
export declare function loadArtifactIgnorePatterns(root: string): Promise<string[]>;
|
package/dist/project.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, readdir } from 'node:fs/promises';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join, relative, resolve } from 'node:path';
|
|
4
4
|
import yaml from 'js-yaml';
|
|
5
|
+
import { ARTIFACT_IGNORE_FILE, isArtifactIgnoredPath, parseArtifactIgnore, } from './artifact-ignore.js';
|
|
5
6
|
import { computeSourceHash } from './hash.js';
|
|
6
7
|
import { parseArtifactStudioManifest } from './manifest.js';
|
|
7
8
|
import { normalizeArtifactPath } from './paths.js';
|
|
@@ -16,7 +17,8 @@ export async function readArtifactStudioSource(rootDir) {
|
|
|
16
17
|
const root = resolve(rootDir);
|
|
17
18
|
const rawManifest = yaml.load(await readFile(join(root, 'artifact.bundle.yml'), 'utf8'));
|
|
18
19
|
const manifest = parseArtifactStudioManifest(rawManifest);
|
|
19
|
-
const
|
|
20
|
+
const ignorePatterns = await loadArtifactIgnorePatterns(root);
|
|
21
|
+
const files = await collectSourceFiles(root, ignorePatterns);
|
|
20
22
|
if (!files.some((file) => file.path === manifest.artifact.entrypoint)) {
|
|
21
23
|
throw new Error(`Entrypoint file not found: ${manifest.artifact.entrypoint}`);
|
|
22
24
|
}
|
|
@@ -26,22 +28,28 @@ export async function readArtifactStudioSource(rootDir) {
|
|
|
26
28
|
files,
|
|
27
29
|
// Hash the RAW manifest, not the parsed one — see computeSourceHash.
|
|
28
30
|
sourceHash: computeSourceHash({ files, manifest: rawManifest }),
|
|
31
|
+
ignorePatterns,
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
|
-
async function collectSourceFiles(root) {
|
|
34
|
+
async function collectSourceFiles(root, ignorePatterns) {
|
|
32
35
|
const files = [];
|
|
33
36
|
async function walk(dir) {
|
|
34
37
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
35
38
|
for (const entry of entries) {
|
|
39
|
+
// Built-in ignores first — `normalizeArtifactPath` rejects `node_modules`
|
|
40
|
+
// (and similar) as source paths, so we must not normalize those names.
|
|
41
|
+
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name))
|
|
42
|
+
continue;
|
|
36
43
|
const absolute = join(dir, entry.name);
|
|
44
|
+
const path = normalizeArtifactPath(relative(root, absolute));
|
|
45
|
+
if (isArtifactIgnoredPath(path, ignorePatterns))
|
|
46
|
+
continue;
|
|
37
47
|
if (entry.isDirectory()) {
|
|
38
|
-
|
|
39
|
-
await walk(absolute);
|
|
48
|
+
await walk(absolute);
|
|
40
49
|
continue;
|
|
41
50
|
}
|
|
42
51
|
if (!entry.isFile())
|
|
43
52
|
continue;
|
|
44
|
-
const path = normalizeArtifactPath(relative(root, absolute));
|
|
45
53
|
if (!isTextFile(path))
|
|
46
54
|
continue;
|
|
47
55
|
files.push({
|
|
@@ -54,9 +62,17 @@ async function collectSourceFiles(root) {
|
|
|
54
62
|
await walk(root);
|
|
55
63
|
return files.sort((a, b) => a.path.localeCompare(b.path));
|
|
56
64
|
}
|
|
65
|
+
export async function loadArtifactIgnorePatterns(root) {
|
|
66
|
+
const ignorePath = join(root, ARTIFACT_IGNORE_FILE);
|
|
67
|
+
if (!existsSync(ignorePath))
|
|
68
|
+
return [];
|
|
69
|
+
return parseArtifactIgnore(await readFile(ignorePath, 'utf8'));
|
|
70
|
+
}
|
|
57
71
|
function isTextFile(path) {
|
|
58
72
|
const ext = path.includes('.') ? path.slice(path.lastIndexOf('.')) : '';
|
|
59
|
-
return path === 'artifact.bundle.yml' ||
|
|
73
|
+
return (path === 'artifact.bundle.yml' ||
|
|
74
|
+
path === ARTIFACT_IGNORE_FILE ||
|
|
75
|
+
TEXT_EXTENSIONS.has(ext));
|
|
60
76
|
}
|
|
61
77
|
function inferContentType(path) {
|
|
62
78
|
if (path.endsWith('.tsx') || path.endsWith('.ts'))
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Vite async-chunk namespace. Keep in sync with atlas `limits.ts` (ARTIFACT_RUNTIME_CHUNK_DIR). */
|
|
2
|
+
export declare const ARTIFACT_RUNTIME_CHUNK_DIR = "chunks";
|
|
3
|
+
export declare function isArtifactRuntimeChunkPath(path: string): boolean;
|
|
4
|
+
export declare function assertSafeRuntimeChunkFileName(fileName: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Vite async-chunk namespace. Keep in sync with atlas `limits.ts` (ARTIFACT_RUNTIME_CHUNK_DIR). */
|
|
2
|
+
export const ARTIFACT_RUNTIME_CHUNK_DIR = 'chunks';
|
|
3
|
+
export function isArtifactRuntimeChunkPath(path) {
|
|
4
|
+
return path === ARTIFACT_RUNTIME_CHUNK_DIR || path.startsWith(`${ARTIFACT_RUNTIME_CHUNK_DIR}/`);
|
|
5
|
+
}
|
|
6
|
+
export function assertSafeRuntimeChunkFileName(fileName) {
|
|
7
|
+
const normalized = fileName.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
8
|
+
if (!isArtifactRuntimeChunkPath(normalized)) {
|
|
9
|
+
throw new Error(`Runtime chunk path must be under ${ARTIFACT_RUNTIME_CHUNK_DIR}/: ${fileName}`);
|
|
10
|
+
}
|
|
11
|
+
if (normalized.includes('..') || normalized.startsWith('/')) {
|
|
12
|
+
throw new Error(`Invalid runtime chunk path: ${fileName}`);
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
package/dist/sdk.d.ts
CHANGED
|
@@ -8,8 +8,18 @@ export interface SequenceApiFetchResponse<TBody = unknown> {
|
|
|
8
8
|
status: number;
|
|
9
9
|
body: TBody;
|
|
10
10
|
}
|
|
11
|
+
export interface SequenceApiUploadOptions {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
/**
|
|
14
|
+
* HTTP method for the multipart request. Defaults to POST; PUT is allowed
|
|
15
|
+
* for update-with-file routes (e.g. replacing an existing disclosure's
|
|
16
|
+
* file). Both are gated by the same `capabilities.api.write` check.
|
|
17
|
+
*/
|
|
18
|
+
method?: 'POST' | 'PUT';
|
|
19
|
+
}
|
|
11
20
|
export interface SequenceApi {
|
|
12
21
|
fetch<TBody = unknown>(path: string, options?: SequenceApiFetchOptions): Promise<SequenceApiFetchResponse<TBody>>;
|
|
22
|
+
upload<TBody = unknown>(path: string, file: File, fields?: Record<string, string>, options?: SequenceApiUploadOptions): Promise<SequenceApiFetchResponse<TBody>>;
|
|
13
23
|
stream(path: string, options?: SequenceApiFetchOptions): Promise<ReadableStream<Uint8Array>>;
|
|
14
24
|
get<TBody = unknown>(path: string): Promise<TBody>;
|
|
15
25
|
post<TBody = unknown>(path: string, body?: unknown): Promise<TBody>;
|
|
@@ -37,6 +37,7 @@ export interface ResolvedSource {
|
|
|
37
37
|
/** Remove any temp materialization. No-op for a local source. Never throws. */
|
|
38
38
|
cleanup: () => Promise<void>;
|
|
39
39
|
}
|
|
40
|
+
export type SourceAuthMode = 'm2m' | 'user';
|
|
40
41
|
export interface ParsedFlags {
|
|
41
42
|
positional: string[];
|
|
42
43
|
flags: Record<string, string | true>;
|
|
@@ -44,9 +45,9 @@ export interface ParsedFlags {
|
|
|
44
45
|
/** True for sources that must be fetched from a remote (git-service / git-url). */
|
|
45
46
|
export declare function isRemoteSpec(spec: SourceSpec): boolean;
|
|
46
47
|
/**
|
|
47
|
-
* Strip
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Strip every URL component that can carry credentials before a git URL is
|
|
49
|
+
* logged or put in an error. Unparseable input is fully withheld because its
|
|
50
|
+
* structure cannot be inspected safely.
|
|
50
51
|
*/
|
|
51
52
|
export declare function redactGitUrl(url: string): string;
|
|
52
53
|
/**
|
|
@@ -57,10 +58,12 @@ export declare function redactGitUrl(url: string): string;
|
|
|
57
58
|
export declare function parseSourceSpec(input: ParsedFlags): SourceSpec;
|
|
58
59
|
/**
|
|
59
60
|
* Resolve a spec to an on-disk source directory + provenance. For git-service
|
|
60
|
-
* the caller must supply `baseUrl` + `token
|
|
61
|
-
*
|
|
61
|
+
* the caller must supply `baseUrl` + `token` and the resolved auth mode;
|
|
62
|
+
* git-url needs no token but rejects a selected M2M principal (it shells out
|
|
63
|
+
* to `git clone`); local needs nothing.
|
|
62
64
|
*/
|
|
63
65
|
export declare function resolveArtifactSource(spec: SourceSpec, opts?: {
|
|
66
|
+
authMode?: SourceAuthMode;
|
|
64
67
|
baseUrl?: string;
|
|
65
68
|
token?: string | null;
|
|
66
69
|
}): Promise<ResolvedSource>;
|
package/dist/source-resolver.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdirSync, rmSync } from 'node:fs';
|
|
|
3
3
|
import { mkdtemp, realpath, rm } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
|
+
import { createScrubbedChildEnv } from './child-environment.js';
|
|
6
7
|
import { materializeRepo, resolveCommitSha, resolveRepo } from './git-service-client.js';
|
|
7
8
|
import { gitServiceCloneUrl, resolveGitPatFromEnv, runGitClone } from './git-clone.js';
|
|
8
9
|
/** True for sources that must be fetched from a remote (git-service / git-url). */
|
|
@@ -10,23 +11,23 @@ export function isRemoteSpec(spec) {
|
|
|
10
11
|
return spec.kind !== 'local';
|
|
11
12
|
}
|
|
12
13
|
/**
|
|
13
|
-
* Strip
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Strip every URL component that can carry credentials before a git URL is
|
|
15
|
+
* logged or put in an error. Unparseable input is fully withheld because its
|
|
16
|
+
* structure cannot be inspected safely.
|
|
16
17
|
*/
|
|
17
18
|
export function redactGitUrl(url) {
|
|
18
19
|
try {
|
|
19
20
|
const parsed = new URL(url);
|
|
20
|
-
if (parsed.username
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
if (!parsed.username && !parsed.password && !parsed.search && !parsed.hash)
|
|
22
|
+
return url;
|
|
23
|
+
parsed.username = '';
|
|
24
|
+
parsed.password = '';
|
|
25
|
+
parsed.search = '';
|
|
26
|
+
parsed.hash = '';
|
|
27
|
+
return parsed.toString();
|
|
26
28
|
}
|
|
27
29
|
catch {
|
|
28
|
-
|
|
29
|
-
return url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, '$1');
|
|
30
|
+
return '<redacted-invalid-git-url>';
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
function flagString(flags, key) {
|
|
@@ -61,27 +62,43 @@ export function parseSourceSpec(input) {
|
|
|
61
62
|
return { kind: 'git-service', namespace: parts[0], name: parts[1], ref };
|
|
62
63
|
}
|
|
63
64
|
if (gitUrl) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = new URL(gitUrl);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error(`--git-url must be a credential-free HTTPS URL (got "${redactGitUrl(gitUrl)}").`);
|
|
71
|
+
}
|
|
72
|
+
if (parsed.protocol !== 'https:' ||
|
|
73
|
+
parsed.username ||
|
|
74
|
+
parsed.password ||
|
|
75
|
+
parsed.search ||
|
|
76
|
+
parsed.hash) {
|
|
77
|
+
throw new Error(`--git-url must be a credential-free HTTPS URL (got "${redactGitUrl(gitUrl)}").`);
|
|
78
|
+
}
|
|
67
79
|
return { kind: 'git-url', url: gitUrl, ref };
|
|
68
80
|
}
|
|
69
81
|
return { kind: 'local', dir: resolve(dir ?? '.') };
|
|
70
82
|
}
|
|
71
83
|
/**
|
|
72
84
|
* Resolve a spec to an on-disk source directory + provenance. For git-service
|
|
73
|
-
* the caller must supply `baseUrl` + `token
|
|
74
|
-
*
|
|
85
|
+
* the caller must supply `baseUrl` + `token` and the resolved auth mode;
|
|
86
|
+
* git-url needs no token but rejects a selected M2M principal (it shells out
|
|
87
|
+
* to `git clone`); local needs nothing.
|
|
75
88
|
*/
|
|
76
89
|
export async function resolveArtifactSource(spec, opts = {}) {
|
|
77
90
|
if (spec.kind === 'local') {
|
|
78
91
|
return { dir: spec.dir, remote: false, provenance: localGitMetadata(spec.dir), cleanup: async () => { } };
|
|
79
92
|
}
|
|
80
|
-
|
|
81
|
-
|
|
93
|
+
if (spec.kind === 'git-url' && opts.authMode === 'm2m') {
|
|
94
|
+
throw new Error('M2M/CI builds only accept platform-managed --repo sources; arbitrary --git-url sources are not trusted in CI.');
|
|
95
|
+
}
|
|
96
|
+
const workspace = await makeTempDir();
|
|
97
|
+
const dest = join(workspace, 'source');
|
|
98
|
+
const cleanup = () => removeDir(workspace);
|
|
82
99
|
try {
|
|
83
100
|
if (spec.kind === 'git-service') {
|
|
84
|
-
if (!opts.baseUrl || !opts.token) {
|
|
101
|
+
if (!opts.authMode || !opts.baseUrl || !opts.token) {
|
|
85
102
|
throw new Error('Deploying from --repo needs a target environment and auth. ' +
|
|
86
103
|
'Pass --env and run `seq-studio login`.');
|
|
87
104
|
}
|
|
@@ -92,6 +109,16 @@ export async function resolveArtifactSource(spec, opts = {}) {
|
|
|
92
109
|
const repo = await resolveRepo({ baseUrl: opts.baseUrl, token: opts.token, namespace: spec.namespace, name: spec.name });
|
|
93
110
|
const ref = spec.ref ?? repo.defaultBranch;
|
|
94
111
|
const sha = await resolveCommitSha({ baseUrl: opts.baseUrl, token: opts.token, repoId: repo.id, ref });
|
|
112
|
+
// CI/system path: the trusted M2M service account can't own a PAT (PAT
|
|
113
|
+
// issuance is gated on a human Auth0 user row), so it keeps the JSON
|
|
114
|
+
// materialize path it has always used, authenticated by its M2M token.
|
|
115
|
+
// Ignore any unrelated human PAT inherited by the parent shell: the
|
|
116
|
+
// selected principal determines both transport and audit identity.
|
|
117
|
+
if (opts.authMode === 'm2m') {
|
|
118
|
+
const count = await materializeRepo({ baseUrl: opts.baseUrl, token: opts.token, repoId: repo.id, ref: sha, destDir: dest });
|
|
119
|
+
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, ${count} file(s), M2M)`);
|
|
120
|
+
return { dir: dest, remote: true, provenance: { gitCommit: sha, gitBranch: ref, gitDirty: false }, cleanup };
|
|
121
|
+
}
|
|
95
122
|
// Human path: materialize via a smart-HTTP `git clone` (one pack
|
|
96
123
|
// transfer). The earlier per-blob JSON walk was O(files) serial HTTP
|
|
97
124
|
// round-trips and stalled/failed on large artifacts. Smart-HTTP
|
|
@@ -104,17 +131,6 @@ export async function resolveArtifactSource(spec, opts = {}) {
|
|
|
104
131
|
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, git clone)`);
|
|
105
132
|
return { dir: dest, remote: true, provenance: { gitCommit: sha, gitBranch: ref, gitDirty: false }, cleanup };
|
|
106
133
|
}
|
|
107
|
-
// CI/system path: the trusted M2M service account can't own a PAT (PAT
|
|
108
|
-
// issuance is gated on a human Auth0 user row), so it keeps the JSON
|
|
109
|
-
// materialize path it has always used, authenticated by its M2M token.
|
|
110
|
-
// Interactive callers (no PAT, no M2M) fall through to the hard error.
|
|
111
|
-
// TODO: teach the git-service smart-HTTP endpoint to accept the trusted
|
|
112
|
-
// M2M identity so CI can use the fast clone path too.
|
|
113
|
-
if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
|
|
114
|
-
const count = await materializeRepo({ baseUrl: opts.baseUrl, token: opts.token, repoId: repo.id, ref: sha, destDir: dest });
|
|
115
|
-
console.log(`[seq-studio] source: ${spec.namespace}/${spec.name}@${ref} (${sha.slice(0, 10)}, ${count} file(s), M2M)`);
|
|
116
|
-
return { dir: dest, remote: true, provenance: { gitCommit: sha, gitBranch: ref, gitDirty: false }, cleanup };
|
|
117
|
-
}
|
|
118
134
|
throw new Error('Deploying from --repo requires a git PAT. Set ATLAS_GIT_PAT to a token with the ' +
|
|
119
135
|
'repo:read scope (create one in Atlas → Settings → Tokens, or `seq-studio auth pat create`). ' +
|
|
120
136
|
'The git-service clone uses smart-HTTP, which authenticates by PAT — the same token you clone the repo with.');
|
|
@@ -137,14 +153,38 @@ export function localGitMetadata(dir) {
|
|
|
137
153
|
};
|
|
138
154
|
try {
|
|
139
155
|
const gitCommit = String(execFileSync('git', ['rev-parse', 'HEAD'], gitOpts)).trim();
|
|
140
|
-
|
|
156
|
+
let gitBranch = String(execFileSync('git', ['branch', '--show-current'], gitOpts)).trim();
|
|
157
|
+
// GitHub Actions (and similar) check out a SHA → detached HEAD even when
|
|
158
|
+
// deploying main. Map that back to `main` only when HEAD equals the tip of
|
|
159
|
+
// main / origin/main — do not trust GITHUB_REF alone (workflow branch can
|
|
160
|
+
// differ from the checked-out SHA).
|
|
161
|
+
if (!gitBranch) {
|
|
162
|
+
gitBranch = resolveDetachedMainBranch({ gitOpts, gitCommit }) ?? '';
|
|
163
|
+
}
|
|
141
164
|
const gitDirty = String(execFileSync('git', ['status', '--porcelain'], gitOpts)).trim().length > 0;
|
|
142
|
-
return { gitCommit, gitBranch, gitDirty };
|
|
165
|
+
return { gitCommit, gitBranch: gitBranch || null, gitDirty };
|
|
143
166
|
}
|
|
144
167
|
catch {
|
|
145
168
|
return { gitCommit: null, gitBranch: null, gitDirty: null };
|
|
146
169
|
}
|
|
147
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* When `git branch --show-current` is empty (detached HEAD), return `main` if
|
|
173
|
+
* HEAD is exactly the tip of a local/remote main ref — otherwise null.
|
|
174
|
+
*/
|
|
175
|
+
function resolveDetachedMainBranch({ gitOpts, gitCommit, }) {
|
|
176
|
+
for (const ref of ['refs/heads/main', 'main', 'refs/remotes/origin/main', 'origin/main']) {
|
|
177
|
+
try {
|
|
178
|
+
const tip = String(execFileSync('git', ['rev-parse', ref], gitOpts)).trim();
|
|
179
|
+
if (tip === gitCommit)
|
|
180
|
+
return 'main';
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// ref missing in this clone (shallow / no remotes) — try the next one
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
148
188
|
async function makeTempDir() {
|
|
149
189
|
const dir = await mkdtemp(join(tmpdir(), 'artifact-src-'));
|
|
150
190
|
// Canonicalize: the Vite html plugin emits the entry file name relative to
|
|
@@ -163,9 +203,18 @@ async function removeDir(dir) {
|
|
|
163
203
|
}
|
|
164
204
|
}
|
|
165
205
|
function cloneGitUrl(url, ref, dest) {
|
|
206
|
+
const homeDir = join(resolve(dest, '..'), 'home');
|
|
207
|
+
mkdirSync(homeDir, { recursive: true });
|
|
166
208
|
const opts = {
|
|
167
209
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
168
210
|
encoding: 'utf8',
|
|
211
|
+
env: createScrubbedChildEnv({
|
|
212
|
+
homeDir,
|
|
213
|
+
additionalEnv: {
|
|
214
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
215
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
169
218
|
};
|
|
170
219
|
const git = (args) => String(execFileSync('git', args, opts)).trim();
|
|
171
220
|
try {
|
|
@@ -0,0 +1,90 @@
|
|
|
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 multipart uploads, use `seq.api.upload(path, file, fields?, options?)`; the bridge constructs `FormData` in the trusted host and returns the usual `{ ok, status, body }` envelope.
|
|
15
|
+
|
|
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
|
+
```yaml
|
|
18
|
+
- /api/crm/customers
|
|
19
|
+
- /api/crm/customers/*
|
|
20
|
+
- /api/lattice/runs/*/advance
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Prerequisite
|
|
24
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
## Package manager
|
|
28
|
+
|
|
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.
|
|
30
|
+
|
|
31
|
+
## Key files
|
|
32
|
+
|
|
33
|
+
| File | Role |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `artifact.bundle.yml` | Capability manifest — every API path the artifact calls: exact paths, `/*` prefix wildcards, or single-segment `*` patterns |
|
|
36
|
+
| `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
|
|
37
|
+
| `src/App.tsx` | Entrypoint UI |
|
|
38
|
+
| `src/lib/api.ts` | `unwrap<T>` envelope helper + re-export of `seq` |
|
|
39
|
+
| `src/styles.css` | Tailwind v4 + `@sequenceholdings/atlas-ui/tokens.css` (+ v2 sheets; build injects them when missing) |
|
|
40
|
+
| `vite.config.ts` | Vite + React + Tailwind v4 plugin |
|
|
41
|
+
|
|
42
|
+
## Defaults
|
|
43
|
+
|
|
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`, `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.
|
|
45
|
+
- Routing: `HashRouter`. Mount routes at `/`, not at the Atlas app prefix.
|
|
46
|
+
- Data: React Query is wired in `src/main.tsx`. Use `useQuery` / `useMutation` against `unwrap(seq.api.*)`.
|
|
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`.
|
|
48
|
+
|
|
49
|
+
## Tokens — colour comes from semantic roles, never a raw ramp
|
|
50
|
+
|
|
51
|
+
This app bundles `@sequenceholdings/atlas-ui/tokens.css` and re-themes per tenant for free. Reach for a **semantic role** and colour follows the host brand and light/dark automatically.
|
|
52
|
+
|
|
53
|
+
**Never** use the numbered `black-*` / `white-*` opacity scales (`text-black-40`, `bg-black-6`, `border-black-10`), raw Tailwind neutrals (`gray|slate|zinc|neutral|stone-N`), arbitrary hex in class strings (`bg-[#1f2937]`), or `var(--black-*)` / `var(--color-black-*)`. These are **retired** — post-cutover they resolve to nothing or to a fixed ink that won't flip in dark. Bare `text-white` / `bg-black` and opacity modifiers like `bg-foreground/10` stay legal; only the numbered steps and raw palettes are banned. There is no old→new lookup table on purpose — pick by *what the element is and what it sits on*.
|
|
54
|
+
|
|
55
|
+
| Situation | Role |
|
|
56
|
+
|---|---|
|
|
57
|
+
| The page surface | `bg-background` — the page only, never a panel |
|
|
58
|
+
| Bordered/rounded panel, card, list | `bg-card` — a border is not a surface; every bordered container needs an explicit surface |
|
|
59
|
+
| Inset well inside a card (code block, read-only value) | `bg-muted` |
|
|
60
|
+
| Form field (input, textarea, select trigger) | `bg-field` |
|
|
61
|
+
| Interactive / hover fill | `bg-accent` / `bg-surface-hover` |
|
|
62
|
+
| Selected / active / focus state fill | `bg-foreground/8`–`bg-foreground/15` overlay |
|
|
63
|
+
| Modal scrim behind a dialog | `bg-overlay` — never `bg-foreground/N` (washes lighter in dark) |
|
|
64
|
+
| Primary text | `text-foreground` |
|
|
65
|
+
| Resting nav items, meta rows | `text-foreground-secondary` |
|
|
66
|
+
| Section labels, captions | `text-muted-foreground` |
|
|
67
|
+
| Standard border | `border-border` (form controls: `border-input`) |
|
|
68
|
+
|
|
69
|
+
Surfaces **nest** — never repeat a rung (`bg-card` inside `bg-card` is invisible): `bg-background` (page) → `bg-card` (panel, + border + `shadow-xs` to lift) → `bg-muted` (inset well).
|
|
70
|
+
|
|
71
|
+
**Dual-runtime.** Inside Atlas the artifact runs in its own iframe and inherits nothing — the runtime posts the host's resolved brand in as inline custom properties (theme channel + `data-tokens`). Standalone `vite dev` has no host, so the artifact must still render on the base semantic roles. **Do not** bundle `tokens.v2.tenants.css`, hardcode an opco's brand, or add `[data-theme]` selectors — that pins the artifact to one tenant.
|
|
72
|
+
|
|
73
|
+
## Bridge methods
|
|
74
|
+
|
|
75
|
+
Two primitives on `seq.api`:
|
|
76
|
+
|
|
77
|
+
- `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`.
|
|
78
|
+
- `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.
|
|
79
|
+
|
|
80
|
+
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.
|
|
81
|
+
|
|
82
|
+
## Lattice embed context
|
|
83
|
+
|
|
84
|
+
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:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const { runId, nodeId, stepId, processId } = seq.lattice.context()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
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).
|
|
@@ -1,66 +1,7 @@
|
|
|
1
1
|
# Sequence Artifact Studio app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Agent instructions for this artifact live in `AGENTS.md` — one file every coding agent reads
|
|
4
|
+
(Cursor and Codex load `AGENTS.md` natively; the import below pulls it in for Claude Code).
|
|
5
|
+
Add or edit guidance in `AGENTS.md`, not here.
|
|
4
6
|
|
|
5
|
-
|
|
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 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
|
-
```yaml
|
|
18
|
-
- /api/crm/customers
|
|
19
|
-
- /api/crm/customers/*
|
|
20
|
-
- /api/lattice/runs/*/advance
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## Prerequisite
|
|
24
|
-
|
|
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.
|
|
26
|
-
|
|
27
|
-
## Package manager
|
|
28
|
-
|
|
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.
|
|
30
|
-
|
|
31
|
-
## Key files
|
|
32
|
-
|
|
33
|
-
| File | Role |
|
|
34
|
-
|---|---|
|
|
35
|
-
| `artifact.bundle.yml` | Capability manifest — every API path the artifact calls: exact paths, `/*` prefix wildcards, or single-segment `*` patterns |
|
|
36
|
-
| `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
|
|
37
|
-
| `src/App.tsx` | Entrypoint UI |
|
|
38
|
-
| `src/lib/api.ts` | `unwrap<T>` envelope helper + re-export of `seq` |
|
|
39
|
-
| `src/styles.css` | Tailwind v4 + `@sequenceholdings/atlas-ui/tokens.css` |
|
|
40
|
-
| `vite.config.ts` | Vite + React + Tailwind v4 plugin |
|
|
41
|
-
|
|
42
|
-
## Defaults
|
|
43
|
-
|
|
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.
|
|
45
|
-
- Routing: `HashRouter`. Mount routes at `/`, not at the Atlas app prefix.
|
|
46
|
-
- Data: React Query is wired in `src/main.tsx`. Use `useQuery` / `useMutation` against `unwrap(seq.api.*)`.
|
|
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`.
|
|
48
|
-
|
|
49
|
-
## Bridge methods
|
|
50
|
-
|
|
51
|
-
Two primitives on `seq.api`:
|
|
52
|
-
|
|
53
|
-
- `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`.
|
|
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.
|
|
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
|
+
@AGENTS.md
|