@sequenceholdings/artifact-studio 0.2.6 → 0.3.1

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 (190) hide show
  1. package/VERSION-PIN.md +12 -1
  2. package/dist/api.d.ts +11 -1
  3. package/dist/api.js +26 -9
  4. package/dist/build.d.ts +2 -2
  5. package/dist/build.js +4 -0
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.js +134 -13
  8. package/dist/dev-link.d.ts +1 -1
  9. package/dist/dev-link.js +2 -1
  10. package/dist/gateway/admission.d.ts +56 -0
  11. package/dist/gateway/admission.js +134 -0
  12. package/dist/gateway/authorization-cache.d.ts +52 -0
  13. package/dist/gateway/authorization-cache.js +111 -0
  14. package/dist/gateway/control-admission.d.ts +48 -0
  15. package/dist/gateway/control-admission.js +127 -0
  16. package/dist/gateway/control.d.ts +94 -0
  17. package/dist/gateway/control.js +116 -0
  18. package/dist/gateway/hosted-proxy.d.ts +36 -0
  19. package/dist/gateway/hosted-proxy.js +344 -0
  20. package/dist/gateway/release-admission.d.ts +23 -0
  21. package/dist/gateway/release-admission.js +259 -0
  22. package/dist/gateway/server.d.ts +38 -0
  23. package/dist/gateway/server.js +382 -0
  24. package/dist/gateway/session.d.ts +72 -0
  25. package/dist/gateway/session.js +144 -0
  26. package/dist/gateway/usage.d.ts +70 -0
  27. package/dist/gateway/usage.js +116 -0
  28. package/dist/gateway-main.d.ts +1 -0
  29. package/dist/gateway-main.js +114 -0
  30. package/dist/git-clone.d.ts +23 -1
  31. package/dist/git-clone.js +32 -15
  32. package/dist/manifest-next.d.ts +217 -0
  33. package/dist/manifest-next.js +132 -0
  34. package/dist/manifest-static.d.ts +76 -0
  35. package/dist/manifest-static.js +124 -0
  36. package/dist/manifest.d.ts +118 -13
  37. package/dist/manifest.js +12 -119
  38. package/dist/next/bridge.d.ts +47 -0
  39. package/dist/next/bridge.js +236 -0
  40. package/dist/next/browser-logs.d.ts +7 -0
  41. package/dist/next/browser-logs.js +51 -0
  42. package/dist/next/browser.d.ts +6 -0
  43. package/dist/next/browser.js +57 -0
  44. package/dist/next/protocol.d.ts +73 -0
  45. package/dist/next/protocol.js +56 -0
  46. package/dist/next/release.d.ts +15 -0
  47. package/dist/next/release.js +46 -0
  48. package/dist/next/requests.d.ts +15 -0
  49. package/dist/next/requests.js +59 -0
  50. package/dist/next/routes.d.ts +4 -0
  51. package/dist/next/routes.js +21 -0
  52. package/dist/next/streams.d.ts +16 -0
  53. package/dist/next/streams.js +132 -0
  54. package/dist/next/theme.d.ts +10 -0
  55. package/dist/next/theme.js +45 -0
  56. package/dist/next/transport.d.ts +15 -0
  57. package/dist/next/transport.js +13 -0
  58. package/dist/next-build.d.ts +29 -0
  59. package/dist/next-build.js +129 -0
  60. package/dist/next-cache/adapter-runtime.d.ts +51 -0
  61. package/dist/next-cache/adapter-runtime.js +73 -0
  62. package/dist/next-cache/component-handler.d.ts +3 -0
  63. package/dist/next-cache/component-handler.js +10 -0
  64. package/dist/next-cache/components.d.ts +20 -0
  65. package/dist/next-cache/components.js +113 -0
  66. package/dist/next-cache/config.d.ts +9 -0
  67. package/dist/next-cache/config.js +105 -0
  68. package/dist/next-cache/incremental-handler.d.ts +18 -0
  69. package/dist/next-cache/incremental-handler.js +17 -0
  70. package/dist/next-cache/incremental.d.ts +27 -0
  71. package/dist/next-cache/incremental.js +99 -0
  72. package/dist/next-cache/index.d.ts +18 -0
  73. package/dist/next-cache/index.js +16 -0
  74. package/dist/next-cache/memory-store.d.ts +27 -0
  75. package/dist/next-cache/memory-store.js +118 -0
  76. package/dist/next-cache/redis-scripts.d.ts +3 -0
  77. package/dist/next-cache/redis-scripts.js +85 -0
  78. package/dist/next-cache/redis-store.d.ts +18 -0
  79. package/dist/next-cache/redis-store.js +56 -0
  80. package/dist/next-cache/runtime.d.ts +29 -0
  81. package/dist/next-cache/runtime.js +63 -0
  82. package/dist/next-cache/serialization.d.ts +39 -0
  83. package/dist/next-cache/serialization.js +333 -0
  84. package/dist/next-cache/store.d.ts +54 -0
  85. package/dist/next-cache/store.js +45 -0
  86. package/dist/next-cache/valkey-transport.d.ts +52 -0
  87. package/dist/next-cache/valkey-transport.js +189 -0
  88. package/dist/next-dev-control.d.ts +48 -0
  89. package/dist/next-dev-control.js +63 -0
  90. package/dist/next-dev-process.d.ts +17 -0
  91. package/dist/next-dev-process.js +110 -0
  92. package/dist/next-dev.d.ts +12 -0
  93. package/dist/next-dev.js +267 -0
  94. package/dist/next-image-config.d.ts +9 -0
  95. package/dist/next-image-config.js +101 -0
  96. package/dist/next-project.d.ts +22 -0
  97. package/dist/next-project.js +64 -0
  98. package/dist/next-public-fonts-local.d.ts +7 -0
  99. package/dist/next-public-fonts-local.js +64 -0
  100. package/dist/next-public-fonts.d.ts +13 -0
  101. package/dist/next-public-fonts.js +49 -0
  102. package/dist/next-source-hash.d.ts +14 -0
  103. package/dist/next-source-hash.js +18 -0
  104. package/dist/next-source.d.ts +35 -0
  105. package/dist/next-source.js +155 -0
  106. package/dist/next.d.ts +9 -0
  107. package/dist/next.js +5 -0
  108. package/dist/project.d.ts +3 -0
  109. package/dist/project.js +9 -1
  110. package/dist/runtime-contracts.d.ts +229 -0
  111. package/dist/runtime-contracts.js +224 -0
  112. package/dist/sdk.d.ts +2 -2
  113. package/dist/server/broker-contract.d.ts +68 -0
  114. package/dist/server/broker-contract.js +33 -0
  115. package/dist/server/client.d.ts +81 -0
  116. package/dist/server/client.js +159 -0
  117. package/dist/server/logger.d.ts +26 -0
  118. package/dist/server/logger.js +24 -0
  119. package/dist/server/release-forms.d.ts +13 -0
  120. package/dist/server/release-forms.js +124 -0
  121. package/dist/server/request-context.d.ts +75 -0
  122. package/dist/server/request-context.js +107 -0
  123. package/dist/server/runtime-config.d.ts +54 -0
  124. package/dist/server/runtime-config.js +64 -0
  125. package/dist/server.d.ts +8 -0
  126. package/dist/server.js +4 -0
  127. package/dist/templates/nextjs/.artifactignore +14 -0
  128. package/dist/templates/nextjs/README.md +157 -0
  129. package/dist/templates/nextjs/app/about/page.tsx +26 -0
  130. package/dist/templates/nextjs/app/actions.ts +18 -0
  131. package/dist/templates/nextjs/app/api/health/route.ts +6 -0
  132. package/dist/templates/nextjs/app/components/counter.tsx +19 -0
  133. package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
  134. package/dist/templates/nextjs/app/globals.css +4 -0
  135. package/dist/templates/nextjs/app/layout.tsx +27 -0
  136. package/dist/templates/nextjs/app/page.tsx +51 -0
  137. package/dist/templates/nextjs/app/providers.tsx +107 -0
  138. package/dist/templates/nextjs/artifact.bundle.yml +55 -0
  139. package/dist/templates/nextjs/gitignore +9 -0
  140. package/dist/templates/nextjs/next.config.mjs +27 -0
  141. package/dist/templates/nextjs/package.json +32 -0
  142. package/dist/templates/nextjs/pnpm-workspace.yaml +22 -0
  143. package/dist/templates/nextjs/postcss.config.mjs +5 -0
  144. package/dist/templates/nextjs/tsconfig.json +20 -0
  145. package/dist/templates/react-vite/package.json +1 -1
  146. package/dist/trusted-install.js +62 -2
  147. package/dist/tunnel/broker.d.ts +75 -0
  148. package/dist/tunnel/broker.js +463 -0
  149. package/dist/tunnel/client.d.ts +22 -0
  150. package/dist/tunnel/client.js +177 -0
  151. package/dist/tunnel/index.d.ts +6 -0
  152. package/dist/tunnel/index.js +4 -0
  153. package/dist/tunnel/io.d.ts +33 -0
  154. package/dist/tunnel/io.js +108 -0
  155. package/dist/tunnel/peer.d.ts +25 -0
  156. package/dist/tunnel/peer.js +147 -0
  157. package/dist/tunnel/protocol.d.ts +78 -0
  158. package/dist/tunnel/protocol.js +210 -0
  159. package/dist/tunnel/redis-auth.d.ts +37 -0
  160. package/dist/tunnel/redis-auth.js +92 -0
  161. package/dist/tunnel/redis-ownership.d.ts +38 -0
  162. package/dist/tunnel/redis-ownership.js +100 -0
  163. package/dist/tunnel/redis-socket.d.ts +13 -0
  164. package/dist/tunnel/redis-socket.js +48 -0
  165. package/dist/tunnel/redis-types.d.ts +75 -0
  166. package/dist/tunnel/redis-types.js +9 -0
  167. package/dist/tunnel/redis.d.ts +56 -0
  168. package/dist/tunnel/redis.js +404 -0
  169. package/dist/tunnel/transport.d.ts +17 -0
  170. package/dist/tunnel/transport.js +1 -0
  171. package/package.json +67 -4
  172. package/templates/nextjs/.artifactignore +14 -0
  173. package/templates/nextjs/README.md +157 -0
  174. package/templates/nextjs/app/about/page.tsx +26 -0
  175. package/templates/nextjs/app/actions.ts +18 -0
  176. package/templates/nextjs/app/api/health/route.ts +6 -0
  177. package/templates/nextjs/app/components/counter.tsx +19 -0
  178. package/templates/nextjs/app/components/greeting-form.tsx +38 -0
  179. package/templates/nextjs/app/globals.css +4 -0
  180. package/templates/nextjs/app/layout.tsx +27 -0
  181. package/templates/nextjs/app/page.tsx +51 -0
  182. package/templates/nextjs/app/providers.tsx +107 -0
  183. package/templates/nextjs/artifact.bundle.yml +55 -0
  184. package/templates/nextjs/gitignore +9 -0
  185. package/templates/nextjs/next.config.mjs +27 -0
  186. package/templates/nextjs/package.json +32 -0
  187. package/templates/nextjs/pnpm-workspace.yaml +22 -0
  188. package/templates/nextjs/postcss.config.mjs +5 -0
  189. package/templates/nextjs/tsconfig.json +20 -0
  190. package/templates/react-vite/package.json +1 -1
package/VERSION-PIN.md CHANGED
@@ -4,12 +4,23 @@ A (re)built Artifact Studio app gets whatever `@sequenceholdings/atlas-ui` the
4
4
  **building CLI** carries — not a semver the artifact's own `package.json`
5
5
  declares.
6
6
 
7
- Artifact Studio 0.2.5 (Studio CLI 0.1.31) pins Atlas UI 3.1.2 exactly.
7
+ Artifact Studio 0.2.7 (Studio CLI 0.1.36) pins Atlas UI 3.3.0 exactly.
8
8
  Ordinary builds now use its canonical, mode-aware `tokens.css`. Rebuilding an
9
9
  existing artifact adopts this toolchain; previously uploaded immutable bundles
10
10
  keep their original CSS. Migrate retired numbered black/white utilities to
11
11
  semantic roles and verify a preview before activating a rebuilt artifact.
12
12
 
13
+ Artifact Studio 0.3.0 (Studio CLI 0.2.0) adds the opt-in Next.js runtime. The
14
+ static compiler continues to pin Atlas UI 3.3.0. Next.js applications own their
15
+ React 19 dependencies and use the `/next` client boundary that Atlas UI 3.4.0
16
+ introduces; they do not inherit the static compiler's aliases. The new packages must be published
17
+ before a fresh external Next.js scaffold can be installed. A local candidate
18
+ build is not evidence of registry availability.
19
+
20
+ Artifact Studio 0.3.1 (Studio CLI 0.2.2) verifies the per-request `jti` that the
21
+ gateway now signs into every request context. The 0.3.0 context schema predates
22
+ that claim and rejects those contexts, so Next.js applications need 0.3.1 or newer.
23
+
13
24
  ## What this means
14
25
 
15
26
  `shared/services/artifact-studio/src/build.ts` force-aliases platform peers via
package/dist/api.d.ts CHANGED
@@ -2,13 +2,23 @@ export interface ApiClientOptions {
2
2
  baseUrl: string;
3
3
  token: string;
4
4
  }
5
+ export declare class ArtifactApiError extends Error {
6
+ readonly status: number;
7
+ constructor(message: string, status: number);
8
+ }
5
9
  export declare function getJson<T>({ baseUrl, token, path }: ApiClientOptions & {
6
10
  path: string;
7
11
  }): Promise<T>;
8
12
  export declare function getJsonOr404<T>({ baseUrl, token, path }: ApiClientOptions & {
9
13
  path: string;
10
14
  }): Promise<T | null>;
11
- export declare function postJson<T>({ baseUrl, token, path, body, }: ApiClientOptions & {
15
+ export declare function postJson<T>({ baseUrl, token, path, body, retry503, signal, }: ApiClientOptions & {
12
16
  path: string;
13
17
  body?: unknown;
18
+ retry503?: boolean;
19
+ signal?: AbortSignal;
14
20
  }): Promise<T>;
21
+ export declare function deleteJson({ baseUrl, token, path, signal }: ApiClientOptions & {
22
+ path: string;
23
+ signal?: AbortSignal;
24
+ }): Promise<void>;
package/dist/api.js CHANGED
@@ -1,4 +1,13 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
1
2
  import { authenticatedRequestUrl } from './deployment-validation.js';
3
+ export class ArtifactApiError extends Error {
4
+ status;
5
+ constructor(message, status) {
6
+ super(message);
7
+ this.status = status;
8
+ this.name = 'ArtifactApiError';
9
+ }
10
+ }
2
11
  const MAX_503_RETRIES = 5;
3
12
  const DEFAULT_RETRY_AFTER_SECONDS = 2;
4
13
  /**
@@ -36,19 +45,20 @@ function extraHeaders() {
36
45
  return {};
37
46
  }
38
47
  }
39
- async function fetchWith503Retry(input, init) {
48
+ async function fetchWith503Retry(input, init, maxRetries = MAX_503_RETRIES) {
40
49
  let attempt = 0;
41
50
  while (true) {
42
51
  const response = await fetch(input, init);
43
- if (response.status !== 503 || attempt >= MAX_503_RETRIES)
52
+ if (response.status !== 503 || attempt >= maxRetries)
44
53
  return response;
45
54
  const retryAfter = Number(response.headers.get('Retry-After')) || DEFAULT_RETRY_AFTER_SECONDS;
46
55
  attempt += 1;
47
56
  console.warn(`[seq-studio] backend warming up (503); retrying in ${retryAfter}s (attempt ${attempt}/${MAX_503_RETRIES})`);
48
- await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
57
+ // An aborted caller (e.g. artifact dev shutting down) must not wait out a Retry-After.
58
+ await delay(retryAfter * 1000, undefined, init.signal ? { signal: init.signal } : undefined);
49
59
  }
50
60
  }
51
- async function authenticatedFetch({ baseUrl, init = {}, path, token, }) {
61
+ async function authenticatedFetch({ baseUrl, init = {}, path, token, retry503 = true, }) {
52
62
  const url = authenticatedRequestUrl({ baseUrl, path });
53
63
  return fetchWith503Retry(url, {
54
64
  ...init,
@@ -58,12 +68,12 @@ async function authenticatedFetch({ baseUrl, init = {}, path, token, }) {
58
68
  ...init.headers,
59
69
  Authorization: `Bearer ${token}`,
60
70
  },
61
- });
71
+ }, retry503 ? MAX_503_RETRIES : 0);
62
72
  }
63
73
  export async function getJson({ baseUrl, token, path }) {
64
74
  const response = await authenticatedFetch({ baseUrl, token, path });
65
75
  if (!response.ok)
66
- throw new Error(await responseError('GET', path, response));
76
+ throw new ArtifactApiError(await responseError('GET', path, response), response.status);
67
77
  return response.json();
68
78
  }
69
79
  export async function getJsonOr404({ baseUrl, token, path }) {
@@ -71,24 +81,31 @@ export async function getJsonOr404({ baseUrl, token, path }) {
71
81
  if (response.status === 404)
72
82
  return null;
73
83
  if (!response.ok)
74
- throw new Error(await responseError('GET', path, response));
84
+ throw new ArtifactApiError(await responseError('GET', path, response), response.status);
75
85
  return response.json();
76
86
  }
77
- export async function postJson({ baseUrl, token, path, body, }) {
87
+ export async function postJson({ baseUrl, token, path, body, retry503 = true, signal, }) {
78
88
  const response = await authenticatedFetch({
79
89
  baseUrl,
80
90
  token,
81
91
  path,
92
+ retry503,
82
93
  init: {
83
94
  method: 'POST',
84
95
  headers: { 'Content-Type': 'application/json' },
85
96
  body: body === undefined ? undefined : JSON.stringify(body),
97
+ ...(signal ? { signal } : {}),
86
98
  },
87
99
  });
88
100
  if (!response.ok)
89
- throw new Error(await responseError('POST', path, response));
101
+ throw new ArtifactApiError(await responseError('POST', path, response), response.status);
90
102
  return response.json();
91
103
  }
104
+ export async function deleteJson({ baseUrl, token, path, signal }) {
105
+ const response = await authenticatedFetch({ baseUrl, token, path, init: { method: 'DELETE', ...(signal ? { signal } : {}) } });
106
+ if (!response.ok)
107
+ throw new ArtifactApiError(await responseError('DELETE', path, response), response.status);
108
+ }
92
109
  /**
93
110
  * Build an actionable error message from a failed response: which request
94
111
  * failed, with what status, and what the server said. A deploy runs several
package/dist/build.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type ObservedBuildEvidence } from './build-provenance.js';
2
2
  import { type ArtifactStudioSourceFile } from './project.js';
3
- import type { ArtifactStudioManifest } from './manifest.js';
3
+ import { type ArtifactStudioStaticManifest } from './manifest.js';
4
4
  import { type ArtifactStudioPeerVersions } from './peer-versions.js';
5
5
  /**
6
6
  * Released Atlas UI 2.0.1 exports its canonical sheet from dist, while the
@@ -17,7 +17,7 @@ export interface ArtifactStudioRuntimeChunk {
17
17
  }
18
18
  export type ArtifactStudioBundleFormat = 'esm';
19
19
  export interface ArtifactStudioBuildResult {
20
- manifest: ArtifactStudioManifest;
20
+ manifest: ArtifactStudioStaticManifest;
21
21
  files: ArtifactStudioSourceFile[];
22
22
  /** Entry module (ESM). Lazy routes land in `chunks`. */
23
23
  bundle: string;
package/dist/build.js CHANGED
@@ -10,6 +10,7 @@ import { collectResolvedStylesheets } from './resolved-stylesheets.js';
10
10
  import { sha256Hex } from './hash.js';
11
11
  import { isArtifactIgnoredPath } from './artifact-ignore.js';
12
12
  import { readArtifactStudioSource } from './project.js';
13
+ import { isNextArtifactManifest } from './manifest.js';
13
14
  import { normalizeArtifactPath } from './paths.js';
14
15
  import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
15
16
  import { ARTIFACT_RUNTIME_CHUNK_DIR, assertSafeRuntimeChunkFileName } from './runtime-chunks.js';
@@ -103,6 +104,9 @@ export async function buildArtifactStudioProject(rootDir, { candidateTokenCompat
103
104
  const selectedAtlasUiDist = resolve(atlasUiPackageDir ?? atlasUiDir, 'dist');
104
105
  const selectedAtlasUiTokens = resolveAtlasUiTokensPath(selectedAtlasUiDir);
105
106
  const source = await readArtifactStudioSource(rootDir);
107
+ if (isNextArtifactManifest(source.manifest)) {
108
+ throw new Error('Next.js applications are built by Atlas. Run seq-studio artifact build --env <env>.');
109
+ }
106
110
  const diagnostics = [
107
111
  ...(hasUnifiedAtlasUiTokens(selectedAtlasUiDir)
108
112
  ? collectDeprecatedTokenImportDiagnostics({ files: source.files })
package/dist/cli.d.ts CHANGED
@@ -5,6 +5,7 @@ interface RemoteProject {
5
5
  id: string;
6
6
  slug: string;
7
7
  title: string;
8
+ projectType?: string;
8
9
  activeDeployment?: {
9
10
  version: string;
10
11
  sourceHash: string;
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
- import { cp, mkdir, writeFile } from 'node:fs/promises';
2
- import { existsSync } from 'node:fs';
1
+ import { copyFile, cp, mkdir, writeFile } from 'node:fs/promises';
2
+ import { constants, existsSync } from 'node:fs';
3
3
  import { dirname, join, relative, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { getJson, getJsonOr404, postJson } from './api.js';
@@ -8,21 +8,22 @@ import { writeBuildArtifact } from './build.js';
8
8
  import { buildArtifactStudioProjectIsolated } from './build-subprocess.js';
9
9
  import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
10
10
  import { prepareArtifactBuildRoot } from './prepare-build.js';
11
- import { parseArtifactStudioManifest } from './manifest.js';
12
- import { hasArtifactStudioManifest, readArtifactStudioSource } from './project.js';
11
+ import { isNextArtifactManifest, parseArtifactStudioManifest } from './manifest.js';
12
+ import { hasArtifactStudioManifest, readArtifactStudioManifest, readArtifactStudioSource } from './project.js';
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
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
+ import { normalizeArtifactPath } from './paths.js';
19
20
  import yaml from 'js-yaml';
20
21
  const COMMANDS = [
21
22
  'init', 'whoami', 'link', 'env', 'status', 'list', 'show',
22
23
  'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
23
24
  ];
24
25
  const USAGE = `usage:
25
- seq-studio artifact init <dir>
26
+ seq-studio artifact init <dir> [--template react-vite|nextjs]
26
27
  seq-studio artifact whoami --env <env>
27
28
  seq-studio artifact link --env <env> [--project <id>]
28
29
  seq-studio artifact env list|use <env>
@@ -33,11 +34,13 @@ const USAGE = `usage:
33
34
  seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--out dist/artifact-bundle.json]
34
35
  seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] --env <env>
35
36
  seq-studio artifact deploy [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] --env <env> [--skip-unchanged] [--no-create] [--project <id>]
36
- seq-studio artifact dev [dir] --env <env> [--once] [--key default] [--link <pkgDir>[,<pkgDir>]]
37
+ seq-studio artifact dev [dir] --env <env> [--once] [--key default] [--link <pkgDir>[,<pkgDir>]] [--port <port>]
37
38
  seq-studio artifact promote <deployment-id> --env <env>
38
39
  seq-studio artifact pull <project-id> --env <env> --out <dir>
39
40
  seq-studio artifact rollback <deployment-id> --env <env>
40
41
 
42
+ Add --help or -h to any artifact command to print this usage without running it.
43
+
41
44
  Source: a local [dir] (default), a platform git-service repo (--repo <ns>/<name>),
42
45
  or a public HTTPS git URL (--git-url <url>). --ref picks a branch/tag/commit (default: the
43
46
  repo's default branch). The target project comes from the source's
@@ -57,6 +60,12 @@ const USAGE = `usage:
57
60
  seq-studio login are still needed to resolve the repo and upload the
58
61
  deployment.)
59
62
 
63
+ Next.js: init --template nextjs, then pnpm install with Node.js 24. dev starts
64
+ a local Next server and an owner-only Atlas preview tunnel (no cloud credentials).
65
+ build --env creates an immutable hosted preview; deploy builds and publishes
66
+ a reviewed Git Service revision through Atlas. For local build checks, use pnpm build.
67
+ --once applies to static artifacts; --port applies to Next development.
68
+
60
69
  Authenticate with: seq-studio login`;
61
70
  export async function runCli(argv = process.argv.slice(2)) {
62
71
  if (argv.some((arg) => arg === '--token' || arg.startsWith('--token='))) {
@@ -64,6 +73,10 @@ export async function runCli(argv = process.argv.slice(2)) {
64
73
  'Authenticate with seq-studio login.');
65
74
  return 1;
66
75
  }
76
+ if (argv.some((arg) => arg === '--help' || arg === '-h')) {
77
+ console.log(USAGE);
78
+ return 0;
79
+ }
67
80
  const parsed = parseArgs(argv);
68
81
  if ('error' in parsed) {
69
82
  console.error(parsed.error);
@@ -118,17 +131,35 @@ async function initCommand(args) {
118
131
  const dir = resolve(args.positional[0] ?? '.');
119
132
  const name = dir.split('/').filter(Boolean).at(-1) ?? 'my-artifact';
120
133
  const slug = slugify(name);
134
+ const template = args.flags.template ?? 'react-vite';
135
+ if (template !== 'react-vite' && template !== 'nextjs') {
136
+ throw new Error('--template must be react-vite or nextjs.');
137
+ }
121
138
  const moduleDir = dirname(fileURLToPath(import.meta.url));
122
- const templateRoot = existsSync(resolve(moduleDir, 'templates', 'react-vite'))
123
- ? resolve(moduleDir, 'templates', 'react-vite')
124
- : resolve(moduleDir, '..', 'templates', 'react-vite');
139
+ const templateRoot = existsSync(resolve(moduleDir, 'templates', template))
140
+ ? resolve(moduleDir, 'templates', template)
141
+ : resolve(moduleDir, '..', 'templates', template);
125
142
  await mkdir(dir, { recursive: true });
126
- await cp(templateRoot, dir, { recursive: true, force: false, errorOnExist: false });
143
+ const templateGitignore = join(templateRoot, 'gitignore');
144
+ await cp(templateRoot, dir, {
145
+ recursive: true, force: false, errorOnExist: false,
146
+ filter: (source) => source !== templateGitignore,
147
+ });
148
+ await installTemplateGitignore({ source: templateGitignore, dir });
127
149
  await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{slug\}\}/g, slug);
128
150
  await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{title\}\}/g, titleize(slug));
129
151
  await replaceInFile(join(dir, 'package.json'), /\{\{slug\}\}/g, slug);
152
+ if (template === 'nextjs') {
153
+ const peers = resolveArtifactStudioPeerVersions();
154
+ await replaceInFile(join(dir, 'package.json'), /\{\{artifactStudioVersion\}\}/g, peers.cliVersion);
155
+ for (const file of ['app/layout.tsx', 'app/page.tsx']) {
156
+ await replaceInFile(join(dir, file), /\{\{title\}\}/g, titleize(slug));
157
+ }
158
+ }
130
159
  console.log(`[seq-studio] initialized ${dir}`);
131
- console.log('Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
160
+ console.log(template === 'nextjs'
161
+ ? 'Next: cd into the app, run pnpm install, then seq-studio login && seq-studio artifact dev --env <env>. Commit pnpm-lock.yaml before deploying. No cloud credentials are required.'
162
+ : 'Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
132
163
  return 0;
133
164
  }
134
165
  async function whoamiCommand(args) {
@@ -184,10 +215,12 @@ async function linkCommand(args) {
184
215
  title: source.manifest.artifact.title,
185
216
  slug: source.manifest.artifact.project_id,
186
217
  description: source.manifest.artifact.description ?? null,
218
+ ...(isNextArtifactManifest(source.manifest) ? { projectType: 'nextjs' } : {}),
187
219
  },
188
220
  });
189
221
  project = created.project;
190
222
  }
223
+ assertProjectRuntime({ project, artifactType: source.manifest.artifact.type, declaredSlug: source.manifest.artifact.project_id });
191
224
  await writeLocalConfig({
192
225
  projectId: project.id,
193
226
  projectSlug: project.slug,
@@ -343,6 +376,12 @@ async function validateCommand(args) {
343
376
  console.log(`[seq-studio] valid ${source.manifest.bundle.name}`);
344
377
  console.log(`[seq-studio] files: ${source.files.length}`);
345
378
  console.log(`[seq-studio] source: ${source.sourceHash}`);
379
+ if (isNextArtifactManifest(source.manifest)) {
380
+ const { validateNextProject } = await import('./next-project.js');
381
+ await validateNextProject({ root: dir, manifest: source.manifest, installed: false });
382
+ console.log('[seq-studio] Next.js source is valid; Atlas performs the release build and hosted policy checks.');
383
+ return 0;
384
+ }
346
385
  reportSandboxWarnings(lintArtifactSandbox(source.files));
347
386
  return 0;
348
387
  }
@@ -372,6 +411,9 @@ async function buildCommand(args) {
372
411
  : {};
373
412
  const source = await resolveArtifactSource(spec, remote);
374
413
  try {
414
+ if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
415
+ return await buildNextCommand({ args, source, active: false });
416
+ }
375
417
  const result = await buildPreparedArtifactSource(source);
376
418
  reportBuildDiagnostics(result);
377
419
  // A remote source's dir is a temp dir we tear down below, so default the
@@ -402,6 +444,11 @@ async function planCommand(args) {
402
444
  token,
403
445
  });
404
446
  try {
447
+ const manifest = await readArtifactStudioManifest(source.dir);
448
+ if (isNextArtifactManifest(manifest)) {
449
+ const { planNextProject } = await import('./next-build.js');
450
+ return await planNextProject({ root: source.dir, manifest, environment: env.name });
451
+ }
405
452
  const result = await buildPreparedArtifactSource(source);
406
453
  reportBuildDiagnostics(result);
407
454
  console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
@@ -458,6 +505,9 @@ async function deployCommand(args) {
458
505
  // the current branch and dirty working tree.
459
506
  if (env.name !== 'local')
460
507
  assertActiveDeployProvenance(source.provenance);
508
+ if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
509
+ return await buildNextCommand({ args, source, active: true });
510
+ }
461
511
  // --skip-unchanged: hash the raw source (no build needed) and no-op when
462
512
  // the remote active deployment already matches *and* was built with the
463
513
  // same CLI/atlas-ui pin. Lets CI redeploy every artifact on every release
@@ -506,12 +556,56 @@ async function deployCommand(args) {
506
556
  await source.cleanup();
507
557
  }
508
558
  }
559
+ async function buildNextCommand({ args, source, active }) {
560
+ const env = await envFromArgs(args);
561
+ const token = await getRequiredToken(args);
562
+ const manifest = await readArtifactStudioManifest(source.dir);
563
+ if (!isNextArtifactManifest(manifest))
564
+ throw new Error('Expected a Next.js application manifest.');
565
+ const { nextSourceRepository, resolveNextRepositoryId, submitNextBuild } = await import('./next-build.js');
566
+ const spec = parseSourceSpec(args);
567
+ const repository = nextSourceRepository({ spec, root: source.dir });
568
+ if (active)
569
+ assertActiveDeployProvenance(source.provenance);
570
+ const repositoryId = active || spec.kind === 'git-service'
571
+ ? await resolveNextRepositoryId({ spec, repository, baseUrl: env.url, token })
572
+ : undefined;
573
+ const project = await ensureRemoteProject({
574
+ env: env.url, token, result: { manifest }, linkConfigDir: spec.kind === 'local' ? source.dir : null,
575
+ allowCreate: args.flags['no-create'] !== true,
576
+ explicitProjectId: typeof args.flags.project === 'string' ? args.flags.project : null,
577
+ });
578
+ if (args.flags['skip-unchanged']) {
579
+ console.log('[seq-studio] Next.js submits source to Atlas for build verification; local static-bundle skip checks do not apply.');
580
+ }
581
+ return submitNextBuild({
582
+ root: source.dir, manifest, projectId: project.id, baseUrl: env.url,
583
+ getToken: () => getRequiredToken(args), provenance: source.provenance, repository, repositoryId,
584
+ channel: active ? 'active' : 'preview', out: typeof args.flags.out === 'string' ? resolve(args.flags.out) : undefined,
585
+ });
586
+ }
509
587
  async function devCommand(args) {
510
588
  if (args.flags.repo || args.flags['git-url']) {
511
589
  throw new Error('dev watches a local folder; --repo / --git-url are only for build, plan, and deploy.');
512
590
  }
513
591
  const dir = resolve(args.positional[0] ?? '.');
514
592
  const env = await envFromArgs(args);
593
+ const initialManifest = await readArtifactStudioManifest(dir);
594
+ if (isNextArtifactManifest(initialManifest)) {
595
+ if (args.flags.once)
596
+ throw new Error('Next.js dev is a live local session. Use artifact build --env <env> for an immutable hosted preview.');
597
+ const auth = await getRequiredAuth(args);
598
+ if (auth.authMode !== 'user')
599
+ throw new Error('Local Next.js development requires a user login. Run seq-studio login.');
600
+ const project = await ensureRemoteProject({ env: env.url, token: auth.token, result: { manifest: initialManifest }, linkConfigDir: dir });
601
+ const { runNextDevelopment } = await import('./next-dev.js');
602
+ return runNextDevelopment({
603
+ root: dir, manifest: initialManifest, projectId: project.id, baseUrl: env.url,
604
+ environment: env.name, previewKey: typeof args.flags.key === 'string' ? args.flags.key : 'default',
605
+ port: typeof args.flags.port === 'string' ? args.flags.port : undefined,
606
+ links: parseLinks(args.flags.link), getToken: () => getRequiredToken(args),
607
+ });
608
+ }
515
609
  // Validate auth up front for a fast failure, but resolve a fresh token on
516
610
  // every push (below) so a long-running watcher survives access-token expiry
517
611
  // instead of dying with "Authentication failed" once the initial token's TTL
@@ -725,9 +819,9 @@ async function pullCommand(args) {
725
819
  path: `/api/artifact-studio/projects/${projectId}/deployments/${projectData.project.activeDeploymentId}/files`,
726
820
  });
727
821
  for (const file of filesData.files) {
728
- const target = join(out, file.path);
822
+ const target = join(out, normalizeArtifactPath(file.path));
729
823
  await mkdir(dirname(target), { recursive: true });
730
- await writeFile(target, file.content, 'utf8');
824
+ await writeFile(target, file.encoding === 'base64' ? Buffer.from(file.content, 'base64') : file.content);
731
825
  }
732
826
  console.log(`[seq-studio] pulled ${projectId} to ${out}`);
733
827
  return 0;
@@ -774,18 +868,21 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
774
868
  throw new Error(`--project ${explicitProjectId} resolves to slug "${data.project.slug}" but this source's ` +
775
869
  `artifact.bundle.yml declares project_id "${result.manifest.artifact.project_id}" — refusing to deploy over a different project`);
776
870
  }
871
+ assertProjectRuntime({ project: data.project, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
777
872
  return data.project;
778
873
  }
779
874
  const config = linkConfigDir ? await readLocalConfig(linkConfigDir) : {};
780
875
  if (config.projectId) {
781
876
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
782
877
  if (data) {
878
+ assertProjectRuntime({ project: data.project, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
783
879
  return data.project;
784
880
  }
785
881
  console.warn(`[seq-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
786
882
  }
787
883
  const existing = await fetchRemoteProject({ env, token, slug: result.manifest.artifact.project_id });
788
884
  if (existing) {
885
+ assertProjectRuntime({ project: existing, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
789
886
  if (linkConfigDir)
790
887
  await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, linkConfigDir);
791
888
  return existing;
@@ -803,12 +900,21 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
803
900
  title: result.manifest.artifact.title,
804
901
  slug: result.manifest.artifact.project_id,
805
902
  description: result.manifest.artifact.description ?? null,
903
+ ...(result.manifest.artifact.type === 'nextjs' ? { projectType: 'nextjs' } : {}),
806
904
  },
807
905
  });
808
906
  if (linkConfigDir)
809
907
  await writeLocalConfig({ ...config, projectId: created.project.id, projectSlug: created.project.slug }, linkConfigDir);
810
908
  return created.project;
811
909
  }
910
+ function assertProjectRuntime({ project, artifactType, declaredSlug }) {
911
+ if (artifactType === 'nextjs' && project.slug !== declaredSlug) {
912
+ throw new Error(`Linked project ${project.slug} does not match artifact.bundle.yml project_id ${declaredSlug}. Relink to the intended project.`);
913
+ }
914
+ if (artifactType === 'nextjs' && project.projectType && project.projectType !== 'nextjs') {
915
+ throw new Error(`Project ${project.slug} uses the static runtime. Create a separate Next.js project in Atlas and update artifact.bundle.yml project_id before linking.`);
916
+ }
917
+ }
812
918
  async function uploadDeployment({ env, token, projectId, result, channel, previewKey, linked = false, provenance, message, }) {
813
919
  // Active deploys omit `version`: the server derives it from its own
814
920
  // sourceHash and de-dupes against the (projectId, version) unique
@@ -894,6 +1000,21 @@ async function resolveAuth(options) {
894
1000
  }
895
1001
  return null;
896
1002
  }
1003
+ /**
1004
+ * npm always leaves `.gitignore` out of published tarballs, so templates ship it
1005
+ * as `gitignore`. Install it without replacing an ignore file the directory has.
1006
+ */
1007
+ async function installTemplateGitignore({ source, dir }) {
1008
+ if (!existsSync(source))
1009
+ return;
1010
+ try {
1011
+ await copyFile(source, join(dir, '.gitignore'), constants.COPYFILE_EXCL);
1012
+ }
1013
+ catch (error) {
1014
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
1015
+ throw error;
1016
+ }
1017
+ }
897
1018
  async function replaceInFile(path, pattern, value) {
898
1019
  const content = await import('node:fs/promises').then((fs) => fs.readFile(path, 'utf8'));
899
1020
  await writeFile(path, content.replace(pattern, value), 'utf8');
@@ -53,7 +53,7 @@ export declare function resolveLinkedDeps({ links, cwd, resolver, readPackageNam
53
53
  */
54
54
  export declare function hashLinkedSources(deps: readonly LinkedDep[]): Promise<string>;
55
55
  /** Run a linked package's `build` script in a child process. */
56
- export declare function rebuildLinkedDep(dep: LinkedDep, signal?: AbortSignal): Promise<void>;
56
+ export declare function rebuildLinkedDep(dep: LinkedDep, signal?: AbortSignal, env?: NodeJS.ProcessEnv): Promise<void>;
57
57
  /**
58
58
  * Copy a linked package's freshly built `dist` and root CSS entries into the
59
59
  * location the artifact build resolves them from. A no-op when the artifact
package/dist/dev-link.js CHANGED
@@ -135,7 +135,7 @@ async function collectFiles(root) {
135
135
  return out;
136
136
  }
137
137
  /** Run a linked package's `build` script in a child process. */
138
- export function rebuildLinkedDep(dep, signal) {
138
+ export function rebuildLinkedDep(dep, signal, env) {
139
139
  return new Promise((resolveBuild, reject) => {
140
140
  if (signal?.aborted) {
141
141
  reject(new Error('build aborted'));
@@ -143,6 +143,7 @@ export function rebuildLinkedDep(dep, signal) {
143
143
  }
144
144
  const child = spawn('pnpm', ['run', 'build'], {
145
145
  cwd: dep.dir,
146
+ ...(env ? { env } : {}),
146
147
  stdio: ['ignore', 'inherit', 'inherit'],
147
148
  });
148
149
  const onAbort = () => {
@@ -0,0 +1,56 @@
1
+ import type { EventEmitter } from 'node:events';
2
+ export interface AdmissionRedis {
3
+ readonly options: {
4
+ enableOfflineQueue?: boolean;
5
+ autoResendUnfulfilledCommands?: boolean;
6
+ maxRetriesPerRequest?: number | null;
7
+ };
8
+ eval(script: string, numberOfKeys: number, ...parameters: (string | number)[]): Promise<unknown>;
9
+ }
10
+ export interface AdmissionTarget {
11
+ environment: string;
12
+ projectId: string;
13
+ deploymentId: string;
14
+ resources?: {
15
+ concurrency?: number;
16
+ max_instances?: number;
17
+ timeout_seconds?: number;
18
+ };
19
+ }
20
+ type ResponseLifecycle = Pick<EventEmitter, 'once' | 'off'> & {
21
+ writableFinished?: boolean;
22
+ destroyed?: boolean;
23
+ };
24
+ export interface AdmissionLease {
25
+ requestId: string;
26
+ /** Redis-server deadline, for tracing; local cancellation uses elapsed time. */
27
+ deadline: number;
28
+ timeoutMs: number;
29
+ release(): Promise<void>;
30
+ /** Register immediately after admission, before starting upstream work.
31
+ * onTimeout must synchronously abort the upstream before the permit is freed. */
32
+ bind(response: ResponseLifecycle, onTimeout: () => void): () => void;
33
+ }
34
+ export declare class ArtifactAdmissionError extends Error {
35
+ readonly status: 429 | 503;
36
+ readonly code: 'capacity_exceeded' | 'admission_unavailable';
37
+ readonly retryAfterSeconds?: number | undefined;
38
+ constructor(status: 429 | 503, code: 'capacity_exceeded' | 'admission_unavailable', retryAfterSeconds?: number | undefined);
39
+ }
40
+ /** Fail-closed admission with no command retries. An uncertain acquisition is
41
+ * never used; its unique permit expires even if Redis accepted it before loss. */
42
+ export declare class ArtifactRequestAdmission {
43
+ private readonly options;
44
+ private readonly namespace;
45
+ private readonly ceiling;
46
+ constructor(options: {
47
+ redis: AdmissionRedis;
48
+ namespace: string;
49
+ platformCeiling?: number;
50
+ onError?: (event: {
51
+ kind: 'admission_release_failed' | 'admission_timeout_callback_failed';
52
+ }) => void;
53
+ });
54
+ acquire(target: AdmissionTarget): Promise<AdmissionLease>;
55
+ }
56
+ export {};