@sequenceholdings/artifact-studio 0.2.1 → 0.2.3

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/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { hasArtifactStudioManifest, readArtifactStudioSource } from './project.j
13
13
  import { localGitMetadata, parseSourceSpec, resolveArtifactSource, } from './source-resolver.js';
14
14
  import { acquireDevLock } from './dev-lock.js';
15
15
  import { runWatchLoop } from './watch-loop.js';
16
- import { hashLinkedSources, parseLinks, rebuildLinkedDep, resolveLinkedDeps, syncLinkedDist, } from './dev-link.js';
16
+ import { hashLinkedSources, hasLinkedAtlasUiToolchain, parseLinks, rebuildLinkedDep, resolveLinkedDeps, syncLinkedDist, } from './dev-link.js';
17
17
  import { lintArtifactSandbox, reportSandboxWarnings } from './sandbox-lint.js';
18
18
  import { assertActiveDeployProvenance } from './active-deploy-policy.js';
19
19
  import yaml from 'js-yaml';
@@ -43,10 +43,12 @@ const USAGE = `usage:
43
43
  repo's default branch). The target project comes from the source's
44
44
  artifact.bundle.yml project_id.
45
45
 
46
- Official (active) deploys require a clean \`main\` checkout with a resolved
47
- git commit — merge first, then \`deploy --repo …\` (or a clean local \`main\`
48
- checkout). Feature branches and dirty trees are preview-only
49
- (\`artifact dev\` / preview channel).
46
+ Official (active) deploys to deployed environments require \`main\` with a
47
+ resolved git commit and a clean artifact source directory. Dirty files
48
+ outside a nested local source directory are ignored with a warning.
49
+ Deployments targeting \`local\` may use any branch, including dirty working
50
+ trees; feature branches and dirty source trees remain preview-only for
51
+ deployed environments (\`artifact dev\` / preview channel).
50
52
 
51
53
  Interactive --repo builds clone over smart-HTTP and require a repo:read git
52
54
  PAT in ATLAS_GIT_PAT — create one with \`seq-studio auth pat create --scopes
@@ -327,6 +329,10 @@ function logActiveDeploymentDetails(deployment) {
327
329
  if (deployment.cliVersion || deployment.atlasUiVersion) {
328
330
  console.log(` peers: artifact-studio@${deployment.cliVersion ?? '?'} atlas-ui@${deployment.atlasUiVersion ?? '?'}`);
329
331
  }
332
+ if (deployment.observedBuildEvidence) {
333
+ console.log(` observed: ${deployment.observedBuildEvidence.channel} / ${deployment.observedBuildEvidence.integrityReceipt?.status ?? 'no receipt'} / ${deployment.observedBuildEvidence.toolchain.selection} toolchain ` +
334
+ `(atlas-ui ${deployment.observedBuildEvidence.atlasUi.contentHash.slice(0, 12)})`);
335
+ }
330
336
  if (deployment.deployMessage)
331
337
  console.log(` message: ${deployment.deployMessage}`);
332
338
  if (deployment.createdAt)
@@ -347,6 +353,11 @@ async function buildPreparedArtifactSource(source) {
347
353
  await prepareArtifactBuildRoot(source.dir, { remote: source.remote });
348
354
  return buildArtifactStudioProjectIsolated(source.dir);
349
355
  }
356
+ function reportBuildDiagnostics(result) {
357
+ for (const diagnostic of result.diagnostics ?? []) {
358
+ console.warn(`[seq-studio] warning: ${diagnostic}`);
359
+ }
360
+ }
350
361
  async function buildCommand(args) {
351
362
  const spec = parseSourceSpec(args);
352
363
  // Local builds stay offline. Public git URLs need no bearer token, but an
@@ -365,6 +376,7 @@ async function buildCommand(args) {
365
376
  const source = await resolveArtifactSource(spec, remote);
366
377
  try {
367
378
  const result = await buildPreparedArtifactSource(source);
379
+ reportBuildDiagnostics(result);
368
380
  // A remote source's dir is a temp dir we tear down below, so default the
369
381
  // bundle output next to the cwd rather than into the throwaway tree.
370
382
  const defaultOutDir = spec.kind === 'local' ? source.dir : process.cwd();
@@ -394,6 +406,7 @@ async function planCommand(args) {
394
406
  });
395
407
  try {
396
408
  const result = await buildPreparedArtifactSource(source);
409
+ reportBuildDiagnostics(result);
397
410
  console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
398
411
  console.log(` source: ${result.sourceHash}`);
399
412
  console.log(` bundle: ${result.bundleHash}`);
@@ -443,9 +456,11 @@ async function deployCommand(args) {
443
456
  token,
444
457
  });
445
458
  try {
446
- // Fail closed before build / project create — active deploys must already
447
- // look like a clean `main` checkout. Preview (`dev`) never hits this path.
448
- assertActiveDeployProvenance(source.provenance);
459
+ // Fail closed before build / project create for deployed environments.
460
+ // Local Atlas is a developer's own target, so it intentionally accepts
461
+ // the current branch and dirty working tree.
462
+ if (env.name !== 'local')
463
+ assertActiveDeployProvenance(source.provenance);
449
464
  // --skip-unchanged: hash the raw source (no build needed) and no-op when
450
465
  // the remote active deployment already matches *and* was built with the
451
466
  // same CLI/atlas-ui pin. Lets CI redeploy every artifact on every release
@@ -472,6 +487,7 @@ async function deployCommand(args) {
472
487
  }
473
488
  }
474
489
  const result = await buildPreparedArtifactSource(source);
490
+ reportBuildDiagnostics(result);
475
491
  warnIfInlinedManifestDriftsFromBundle(result);
476
492
  // Local sources read/write the project link in their working dir; remote
477
493
  // sources resolve strictly by manifest project_id (never a stray config.json
@@ -517,6 +533,7 @@ async function devCommand(args) {
517
533
  // refresh the artifact automatically, instead of the rebuild/reinstall/restart
518
534
  // dance. Resolved once up front so a bad path fails fast.
519
535
  const linkedDeps = resolveLinkedDeps({ links: parseLinks(args.flags.link) });
536
+ const candidateTokenCompatibility = hasLinkedAtlasUiToolchain({ deps: linkedDeps });
520
537
  for (const dep of linkedDeps) {
521
538
  const where = relative(process.cwd(), dep.dir) || '.';
522
539
  console.log(`[seq-studio] linked ${dep.name} ← ${where}${dep.inPlace ? ' (in place)' : ''}`);
@@ -555,7 +572,11 @@ async function devCommand(args) {
555
572
  // Vite builds leak memory that GC can't reclaim and eventually OOM a
556
573
  // long-running watch. Isolating each build keeps memory flat. The signal
557
574
  // reaps the child if the watcher is shutting down mid-build.
558
- const result = await buildArtifactStudioProjectIsolated(dir, { signal: controller.signal });
575
+ const result = await buildArtifactStudioProjectIsolated(dir, {
576
+ signal: controller.signal,
577
+ candidateTokenCompatibility,
578
+ });
579
+ reportBuildDiagnostics(result);
559
580
  warnIfInlinedManifestDriftsFromBundle(result);
560
581
  // The tree can change between the cheap pre-read above and the build
561
582
  // finishing — including reverting back to the last-shipped snapshot. Trust
@@ -580,6 +601,7 @@ async function devCommand(args) {
580
601
  result,
581
602
  channel: 'preview',
582
603
  previewKey,
604
+ linked: linkedDeps.length > 0,
583
605
  provenance: localGitMetadata(dir),
584
606
  message: `Preview push ${buildCount}`,
585
607
  });
@@ -811,7 +833,7 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
811
833
  await writeLocalConfig({ ...config, projectId: created.project.id, projectSlug: created.project.slug }, linkConfigDir);
812
834
  return created.project;
813
835
  }
814
- async function uploadDeployment({ env, token, projectId, result, channel, previewKey, provenance, message, }) {
836
+ async function uploadDeployment({ env, token, projectId, result, channel, previewKey, linked = false, provenance, message, }) {
815
837
  // Active deploys omit `version`: the server derives it from its own
816
838
  // sourceHash and de-dupes against the (projectId, version) unique
817
839
  // constraint. Pinning it client-side is how a stale CLI-side hash turned
@@ -834,12 +856,18 @@ async function uploadDeployment({ env, token, projectId, result, channel, previe
834
856
  bundleFormat: result.bundleFormat,
835
857
  sourceMap: result.sourceMap,
836
858
  ...(version ? { version } : {}),
837
- channel,
838
859
  previewKey,
839
860
  deployMessage: message ?? null,
840
- cliVersion: result.peerVersions.cliVersion,
841
- atlasUiVersion: result.peerVersions.atlasUiVersion,
842
- ...provenance,
861
+ observedBuildEvidence: {
862
+ ...result.observedBuildEvidence,
863
+ toolchain: { ...result.observedBuildEvidence.toolchain, selection: linked ? 'linked' : 'published' },
864
+ channel,
865
+ sourceRevision: {
866
+ commit: provenance.gitCommit,
867
+ branch: provenance.gitBranch,
868
+ dirty: provenance.gitDirty,
869
+ },
870
+ },
843
871
  },
844
872
  });
845
873
  return data.deployment;
@@ -9,8 +9,8 @@
9
9
  *
10
10
  * `--link` collapses that to zero steps: the watcher hashes the linked package's
11
11
  * source each tick, and on change rebuilds the package and copies its fresh `dist`
12
- * into the exact location the artifact build resolves it from — then forces an
13
- * artifact rebuild + preview push.
12
+ * plus package-root CSS entries into the exact location the artifact build resolves
13
+ * them from — then forces an artifact rebuild + preview push.
14
14
  */
15
15
  export interface LinkedDep {
16
16
  /** Absolute path to the linked source package (e.g. shared/services/atlas-ui). */
@@ -26,6 +26,10 @@ export interface LinkedDep {
26
26
  /** True when the artifact resolves the package straight from `dir` (symlinked file: dep). */
27
27
  inPlace: boolean;
28
28
  }
29
+ /** True only when this dev session explicitly selected a local Atlas UI package. */
30
+ export declare function hasLinkedAtlasUiToolchain({ deps }: {
31
+ deps: readonly LinkedDep[];
32
+ }): boolean;
29
33
  /** Resolve `${name}/package.json` the same way the build worker does. */
30
34
  type PackageResolver = (request: string) => string;
31
35
  /** Split a `--link a,b` flag value into trimmed, non-empty paths. */
@@ -51,10 +55,10 @@ export declare function hashLinkedSources(deps: readonly LinkedDep[]): Promise<s
51
55
  /** Run a linked package's `build` script in a child process. */
52
56
  export declare function rebuildLinkedDep(dep: LinkedDep, signal?: AbortSignal): Promise<void>;
53
57
  /**
54
- * Copy a linked package's freshly built `dist` into the location the artifact
55
- * build resolves it from. A no-op when the artifact already resolves the package
56
- * in place (symlinked file: dep), where the rebuild already wrote the bytes the
57
- * build will read.
58
+ * Copy a linked package's freshly built `dist` and root CSS entries into the
59
+ * location the artifact build resolves them from. A no-op when the artifact
60
+ * already resolves the package in place (symlinked file: dep), where the rebuild
61
+ * already wrote the bytes the build will read.
58
62
  */
59
63
  export declare function syncLinkedDist(dep: LinkedDep): Promise<void>;
60
64
  export {};
package/dist/dev-link.js CHANGED
@@ -1,9 +1,13 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
4
- import { cp, readFile, readdir, rm } from 'node:fs/promises';
4
+ import { cp, mkdir, readFile, readdir, rm } from 'node:fs/promises';
5
5
  import { createRequire } from 'node:module';
6
6
  import { dirname, join, relative, resolve } from 'node:path';
7
+ /** True only when this dev session explicitly selected a local Atlas UI package. */
8
+ export function hasLinkedAtlasUiToolchain({ deps }) {
9
+ return deps.some((dep) => dep.name === '@sequenceholdings/atlas-ui');
10
+ }
7
11
  const WATCHED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.css', '.json']);
8
12
  const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', '.next', '.turbo', '.cache']);
9
13
  const defaultResolver = createRequire(import.meta.url).resolve;
@@ -72,9 +76,26 @@ export async function hashLinkedSources(deps) {
72
76
  for (const dep of deps) {
73
77
  hash.update(dep.name);
74
78
  hash.update('\0');
75
- const files = (await collectFiles(dep.srcDir)).sort((a, b) => a.localeCompare(b));
76
- for (const file of files) {
77
- hash.update(relative(dep.srcDir, file));
79
+ const sourceRoots = [
80
+ { label: 'src', dir: dep.srcDir },
81
+ // Atlas UI emits its package-root token entries from this generator.
82
+ // Watching scripts means edits to that generator trigger a linked preview.
83
+ { label: 'scripts', dir: join(dep.dir, 'scripts') },
84
+ ];
85
+ for (const sourceRoot of sourceRoots) {
86
+ const files = (await collectFiles(sourceRoot.dir)).sort((a, b) => a.localeCompare(b));
87
+ for (const file of files) {
88
+ hash.update(sourceRoot.label);
89
+ hash.update('/');
90
+ hash.update(relative(sourceRoot.dir, file));
91
+ hash.update('\0');
92
+ hash.update(await readFile(file));
93
+ hash.update('\0');
94
+ }
95
+ }
96
+ for (const file of await packageRootCssFiles(dep.dir)) {
97
+ hash.update('root-css/');
98
+ hash.update(relative(dep.dir, file));
78
99
  hash.update('\0');
79
100
  hash.update(await readFile(file));
80
101
  hash.update('\0');
@@ -82,6 +103,14 @@ export async function hashLinkedSources(deps) {
82
103
  }
83
104
  return hash.digest('hex');
84
105
  }
106
+ async function packageRootCssFiles(pkgDir) {
107
+ if (!existsSync(pkgDir))
108
+ return [];
109
+ const entries = await readdir(pkgDir, { withFileTypes: true });
110
+ return entries
111
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.css'))
112
+ .map((entry) => join(pkgDir, entry.name));
113
+ }
85
114
  async function collectFiles(root) {
86
115
  if (!existsSync(root))
87
116
  return [];
@@ -139,10 +168,10 @@ export function rebuildLinkedDep(dep, signal) {
139
168
  });
140
169
  }
141
170
  /**
142
- * Copy a linked package's freshly built `dist` into the location the artifact
143
- * build resolves it from. A no-op when the artifact already resolves the package
144
- * in place (symlinked file: dep), where the rebuild already wrote the bytes the
145
- * build will read.
171
+ * Copy a linked package's freshly built `dist` and root CSS entries into the
172
+ * location the artifact build resolves them from. A no-op when the artifact
173
+ * already resolves the package in place (symlinked file: dep), where the rebuild
174
+ * already wrote the bytes the build will read.
146
175
  */
147
176
  export async function syncLinkedDist(dep) {
148
177
  if (dep.inPlace)
@@ -150,6 +179,11 @@ export async function syncLinkedDist(dep) {
150
179
  if (!existsSync(dep.distDir)) {
151
180
  throw new Error(`${dep.name} build produced no dist at ${dep.distDir}`);
152
181
  }
182
+ const targetPkgDir = dirname(dep.targetDistDir);
183
+ await mkdir(targetPkgDir, { recursive: true });
153
184
  await rm(dep.targetDistDir, { recursive: true, force: true });
154
185
  await cp(dep.distDir, dep.targetDistDir, { recursive: true });
186
+ for (const file of await packageRootCssFiles(dep.dir)) {
187
+ await cp(file, join(targetPkgDir, relative(dep.dir, file)));
188
+ }
155
189
  }
@@ -58,9 +58,9 @@ export declare const artifactStudioManifestSchema: z.ZodObject<{
58
58
  targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
59
59
  url: z.ZodString;
60
60
  visibility: z.ZodDefault<z.ZodEnum<{
61
+ published: "published";
61
62
  private: "private";
62
63
  shared: "shared";
63
- published: "published";
64
64
  }>>;
65
65
  }, z.core.$strip>>>;
66
66
  sidebarBadge: z.ZodOptional<z.ZodObject<{
@@ -0,0 +1,17 @@
1
+ /** Max length of user-authored SQL accepted by reporting routes. */
2
+ export declare const REPORTING_MAX_SQL_CHARS = 500000;
3
+ /** Max decoded file size for manual report uploads. */
4
+ export declare const UPLOAD_MAX_BYTES: number;
5
+ /** Max length of a base64-encoded upload before decoding. */
6
+ export declare const UPLOAD_MAX_BASE64_CHARS: number;
7
+ /** Formatted bundle transport and file boundaries. */
8
+ export declare const FORMATTED_BUNDLE_MAX_BYTES: number;
9
+ /** Reserve space for multipart boundaries and per-file headers in the artifact. */
10
+ export declare const FORMATTED_BUNDLE_MULTIPART_OVERHEAD_RESERVE_BYTES: number;
11
+ export declare const FORMATTED_BUNDLE_MAX_DEFINITION_BYTES: number;
12
+ export declare const FORMATTED_BUNDLE_MAX_SQL_FILES = 50;
13
+ export declare const FORMATTED_BUNDLE_MAX_SQL_BYTES: number;
14
+ export declare const FORMATTED_BUNDLE_MAX_SQL_CHARS = 500000;
15
+ export declare const FORMATTED_BUNDLE_MAX_TOTAL_SQL_CHARS: number;
16
+ /** The definition and multipart contracts must reject the same input count. */
17
+ export declare const MAX_FORMATTED_REPORT_INPUTS = 50;
@@ -0,0 +1,17 @@
1
+ /** Max length of user-authored SQL accepted by reporting routes. */
2
+ export const REPORTING_MAX_SQL_CHARS = 500_000;
3
+ /** Max decoded file size for manual report uploads. */
4
+ export const UPLOAD_MAX_BYTES = 20 * 1024 * 1024;
5
+ /** Max length of a base64-encoded upload before decoding. */
6
+ export const UPLOAD_MAX_BASE64_CHARS = Math.ceil(UPLOAD_MAX_BYTES / 3) * 4;
7
+ /** Formatted bundle transport and file boundaries. */
8
+ export const FORMATTED_BUNDLE_MAX_BYTES = 20 * 1024 * 1024;
9
+ /** Reserve space for multipart boundaries and per-file headers in the artifact. */
10
+ export const FORMATTED_BUNDLE_MULTIPART_OVERHEAD_RESERVE_BYTES = 64 * 1024;
11
+ export const FORMATTED_BUNDLE_MAX_DEFINITION_BYTES = 2 * 1024 * 1024;
12
+ export const FORMATTED_BUNDLE_MAX_SQL_FILES = 50;
13
+ export const FORMATTED_BUNDLE_MAX_SQL_BYTES = 2 * 1024 * 1024;
14
+ export const FORMATTED_BUNDLE_MAX_SQL_CHARS = REPORTING_MAX_SQL_CHARS;
15
+ export const FORMATTED_BUNDLE_MAX_TOTAL_SQL_CHARS = 10 * REPORTING_MAX_SQL_CHARS;
16
+ /** The definition and multipart contracts must reject the same input count. */
17
+ export const MAX_FORMATTED_REPORT_INPUTS = FORMATTED_BUNDLE_MAX_SQL_FILES;
package/dist/sdk.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface SequenceApiUploadOptions {
19
19
  }
20
20
  export interface SequenceApi {
21
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>>;
22
+ upload<TBody = unknown>(path: string, file: File | readonly File[], fields?: Record<string, string>, options?: SequenceApiUploadOptions): Promise<SequenceApiFetchResponse<TBody>>;
23
23
  stream(path: string, options?: SequenceApiFetchOptions): Promise<ReadableStream<Uint8Array>>;
24
24
  get<TBody = unknown>(path: string): Promise<TBody>;
25
25
  post<TBody = unknown>(path: string, body?: unknown): Promise<TBody>;
@@ -88,7 +88,7 @@ export interface SequenceLattice {
88
88
  export interface SequenceHost {
89
89
  /**
90
90
  * Navigate the parent Atlas shell to an in-app route — e.g.
91
- * `seq.host.navigate('/crm#/customers/<uid>')`. Fire-and-forget: the artifact
91
+ * `seq.host.navigate('/relationships#/customers/<uid>')`. Fire-and-forget: the artifact
92
92
  * sandbox cannot navigate the top frame itself (no `allow-top-navigation`),
93
93
  * so this posts the request to the host, which validates it and drives the
94
94
  * shell router. The host ignores anything that isn't a same-app relative
@@ -0,0 +1,13 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface ArtifactSourceBoundaryOptions {
3
+ root: string;
4
+ trustedExternalRoots: readonly string[];
5
+ }
6
+ /**
7
+ * Artifact source must be self-contained: allowing a relative import to escape
8
+ * the build root would let uncommitted files elsewhere in a containing
9
+ * checkout enter the official bundle without being covered by its dirty bit.
10
+ * Published dependencies and the CLI-pinned platform peers are intentionally
11
+ * allowed because they are external to the artifact by design.
12
+ */
13
+ export declare function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }: ArtifactSourceBoundaryOptions): Plugin;
@@ -0,0 +1,54 @@
1
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
2
+ /**
3
+ * Artifact source must be self-contained: allowing a relative import to escape
4
+ * the build root would let uncommitted files elsewhere in a containing
5
+ * checkout enter the official bundle without being covered by its dirty bit.
6
+ * Published dependencies and the CLI-pinned platform peers are intentionally
7
+ * allowed because they are external to the artifact by design.
8
+ */
9
+ export function rejectOutsideSourcePlugin({ root, trustedExternalRoots, }) {
10
+ const rootResolved = resolve(root);
11
+ const trustedRoots = trustedExternalRoots.map((trustedRoot) => resolve(trustedRoot));
12
+ return {
13
+ name: 'reject-artifact-outside-source',
14
+ enforce: 'pre',
15
+ async resolveId(source, importer, options) {
16
+ const resolved = await this.resolve(source, importer, { ...options, skipSelf: true });
17
+ if (!resolved || resolved.external || resolved.id.startsWith('\0'))
18
+ return resolved;
19
+ const filePath = resolved.id.split('?')[0];
20
+ if (!isAbsolute(filePath) || isPathWithin(rootResolved, filePath))
21
+ return resolved;
22
+ const importerPath = importer?.split('?')[0];
23
+ const importerIsTrustedExternal = importerPath != null &&
24
+ (isNodeModulesPath(importerPath) || isTrustedExternalPath(importerPath, trustedRoots));
25
+ if (isNodeModulesPath(filePath) && (isBareImport(source) || importerIsTrustedExternal)) {
26
+ return resolved;
27
+ }
28
+ if (isTrustedExternalPath(filePath, trustedRoots))
29
+ return resolved;
30
+ const from = importer
31
+ ? ` (imported from ${relative(rootResolved, importer.split('?')[0])})`
32
+ : '';
33
+ throw new Error(`Artifact import "${filePath}" resolves outside artifact source directory ` +
34
+ `"${rootResolved}"${from}. Keep relative build inputs inside the artifact ` +
35
+ 'source directory or publish shared code as a dependency.');
36
+ },
37
+ };
38
+ }
39
+ function isPathWithin(root, candidate) {
40
+ const pathFromRoot = relative(root, candidate);
41
+ return (pathFromRoot === '' ||
42
+ (pathFromRoot !== '..' &&
43
+ !pathFromRoot.startsWith(`..${sep}`) &&
44
+ !isAbsolute(pathFromRoot)));
45
+ }
46
+ function isTrustedExternalPath(filePath, trustedRoots) {
47
+ return trustedRoots.some((trustedRoot) => isPathWithin(trustedRoot, filePath));
48
+ }
49
+ function isNodeModulesPath(filePath) {
50
+ return filePath.includes(`${sep}node_modules${sep}`);
51
+ }
52
+ function isBareImport(source) {
53
+ return !source.startsWith('.') && !isAbsolute(source);
54
+ }
@@ -161,7 +161,12 @@ export function localGitMetadata(dir) {
161
161
  if (!gitBranch) {
162
162
  gitBranch = resolveDetachedMainBranch({ gitOpts, gitCommit }) ?? '';
163
163
  }
164
- const gitDirty = String(execFileSync('git', ['status', '--porcelain'], gitOpts)).trim().length > 0;
164
+ const wholeTreeStatus = String(execFileSync('git', ['status', '--porcelain'], gitOpts)).trim();
165
+ const sourceStatus = String(execFileSync('git', ['status', '--porcelain', '--', '.'], gitOpts)).trim();
166
+ const gitDirty = sourceStatus.length > 0;
167
+ if (wholeTreeStatus && !gitDirty) {
168
+ console.warn(`[seq-studio] ignoring unrelated dirty files outside artifact source directory ${dir}`);
169
+ }
165
170
  return { gitCommit, gitBranch: gitBranch || null, gitDirty };
166
171
  }
167
172
  catch {
@@ -36,7 +36,7 @@ Use **pnpm** for artifacts you will deploy from git — `seq-studio artifact bui
36
36
  | `src/main.tsx` | Providers: `QueryClientProvider` → `PortalContainerProvider` → `App` |
37
37
  | `src/App.tsx` | Entrypoint UI |
38
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) |
39
+ | `src/styles.css` | Tailwind v4 + canonical `@sequenceholdings/atlas-ui/tokens.css` |
40
40
  | `vite.config.ts` | Vite + React + Tailwind v4 plugin |
41
41
 
42
42
  ## Defaults
@@ -9,15 +9,15 @@
9
9
  },
10
10
  "dependencies": {},
11
11
  "devDependencies": {
12
- "@sequenceholdings/artifact-studio": "^0.1.0",
13
- "@sequenceholdings/atlas-ui": "^0.1.0",
12
+ "@sequenceholdings/artifact-studio": "^0.2.1",
13
+ "@sequenceholdings/atlas-ui": "^2.0.1",
14
14
  "@tailwindcss/postcss": "^4.2.2",
15
15
  "@tailwindcss/vite": "^4.2.2",
16
16
  "@tanstack/react-query": "^4.36.1",
17
17
  "@vitejs/plugin-react": "^5.0.0",
18
18
  "react": "^18.2.0",
19
19
  "react-dom": "^18.2.0",
20
- "react-router-dom": "^6.28.0",
20
+ "react-router-dom": "^7.18.2",
21
21
  "sonner": "^2.0.7",
22
22
  "tailwindcss": "^4.2.2",
23
23
  "tw-animate-css": "^1.4.0",
@@ -1,8 +1,6 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
3
  @import "@sequenceholdings/atlas-ui/tokens.css";
4
- @import "@sequenceholdings/atlas-ui/tokens.v2.css";
5
- @import "@sequenceholdings/atlas-ui/theme.v2.css";
6
4
 
7
5
  /* Tailwind v4 skips node_modules during automatic source detection, so the
8
6
  utility classes compiled into atlas-ui's components are never generated under
@@ -0,0 +1,16 @@
1
+ import type { ArtifactStudioSourceFile } from './project.js';
2
+ export interface RetiredOpacityUtilityReference {
3
+ path: string;
4
+ utility: string;
5
+ }
6
+ export declare function collectDeprecatedTokenImportDiagnostics({ files, }: {
7
+ files: readonly ArtifactStudioSourceFile[];
8
+ }): string[];
9
+ /**
10
+ * Candidate builds use a source report rather than a build-time ban. A normal
11
+ * CLI release must remain able to rebuild pre-cutover artifacts; an explicit
12
+ * `artifact dev --link <atlas-ui>` preview is the opt-in migration surface.
13
+ */
14
+ export declare function collectCandidateTokenCompatibilityDiagnostics({ files, }: {
15
+ files: readonly ArtifactStudioSourceFile[];
16
+ }): string[];