@sequenceholdings/artifact-studio 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/VERSION-PIN.md +8 -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 +105 -10
  8. package/dist/dev-link.d.ts +1 -1
  9. package/dist/dev-link.js +2 -1
  10. package/dist/gateway/admission.d.ts +55 -0
  11. package/dist/gateway/admission.js +132 -0
  12. package/dist/gateway/control.d.ts +91 -0
  13. package/dist/gateway/control.js +113 -0
  14. package/dist/gateway/hosted-proxy.d.ts +36 -0
  15. package/dist/gateway/hosted-proxy.js +347 -0
  16. package/dist/gateway/release-admission.d.ts +23 -0
  17. package/dist/gateway/release-admission.js +259 -0
  18. package/dist/gateway/server.d.ts +29 -0
  19. package/dist/gateway/server.js +298 -0
  20. package/dist/gateway/session.d.ts +71 -0
  21. package/dist/gateway/session.js +143 -0
  22. package/dist/gateway/usage.d.ts +37 -0
  23. package/dist/gateway/usage.js +46 -0
  24. package/dist/gateway-main.d.ts +1 -0
  25. package/dist/gateway-main.js +84 -0
  26. package/dist/git-clone.d.ts +23 -1
  27. package/dist/git-clone.js +32 -15
  28. package/dist/manifest-next.d.ts +217 -0
  29. package/dist/manifest-next.js +120 -0
  30. package/dist/manifest-static.d.ts +76 -0
  31. package/dist/manifest-static.js +124 -0
  32. package/dist/manifest.d.ts +118 -13
  33. package/dist/manifest.js +12 -119
  34. package/dist/next/bridge.d.ts +47 -0
  35. package/dist/next/bridge.js +229 -0
  36. package/dist/next/browser-logs.d.ts +7 -0
  37. package/dist/next/browser-logs.js +51 -0
  38. package/dist/next/browser.d.ts +6 -0
  39. package/dist/next/browser.js +57 -0
  40. package/dist/next/protocol.d.ts +73 -0
  41. package/dist/next/protocol.js +56 -0
  42. package/dist/next/release.d.ts +15 -0
  43. package/dist/next/release.js +46 -0
  44. package/dist/next/requests.d.ts +15 -0
  45. package/dist/next/requests.js +59 -0
  46. package/dist/next/routes.d.ts +4 -0
  47. package/dist/next/routes.js +21 -0
  48. package/dist/next/streams.d.ts +16 -0
  49. package/dist/next/streams.js +132 -0
  50. package/dist/next/theme.d.ts +10 -0
  51. package/dist/next/theme.js +45 -0
  52. package/dist/next/transport.d.ts +15 -0
  53. package/dist/next/transport.js +13 -0
  54. package/dist/next-build.d.ts +29 -0
  55. package/dist/next-build.js +129 -0
  56. package/dist/next-cache/adapter-runtime.d.ts +51 -0
  57. package/dist/next-cache/adapter-runtime.js +73 -0
  58. package/dist/next-cache/component-handler.d.ts +3 -0
  59. package/dist/next-cache/component-handler.js +10 -0
  60. package/dist/next-cache/components.d.ts +20 -0
  61. package/dist/next-cache/components.js +113 -0
  62. package/dist/next-cache/config.d.ts +9 -0
  63. package/dist/next-cache/config.js +78 -0
  64. package/dist/next-cache/incremental-handler.d.ts +18 -0
  65. package/dist/next-cache/incremental-handler.js +17 -0
  66. package/dist/next-cache/incremental.d.ts +27 -0
  67. package/dist/next-cache/incremental.js +99 -0
  68. package/dist/next-cache/index.d.ts +18 -0
  69. package/dist/next-cache/index.js +16 -0
  70. package/dist/next-cache/memory-store.d.ts +27 -0
  71. package/dist/next-cache/memory-store.js +118 -0
  72. package/dist/next-cache/redis-scripts.d.ts +3 -0
  73. package/dist/next-cache/redis-scripts.js +85 -0
  74. package/dist/next-cache/redis-store.d.ts +18 -0
  75. package/dist/next-cache/redis-store.js +56 -0
  76. package/dist/next-cache/runtime.d.ts +29 -0
  77. package/dist/next-cache/runtime.js +63 -0
  78. package/dist/next-cache/serialization.d.ts +39 -0
  79. package/dist/next-cache/serialization.js +333 -0
  80. package/dist/next-cache/store.d.ts +54 -0
  81. package/dist/next-cache/store.js +45 -0
  82. package/dist/next-cache/valkey-transport.d.ts +52 -0
  83. package/dist/next-cache/valkey-transport.js +189 -0
  84. package/dist/next-dev-control.d.ts +48 -0
  85. package/dist/next-dev-control.js +63 -0
  86. package/dist/next-dev-process.d.ts +17 -0
  87. package/dist/next-dev-process.js +110 -0
  88. package/dist/next-dev.d.ts +12 -0
  89. package/dist/next-dev.js +267 -0
  90. package/dist/next-image-config.d.ts +9 -0
  91. package/dist/next-image-config.js +101 -0
  92. package/dist/next-project.d.ts +22 -0
  93. package/dist/next-project.js +64 -0
  94. package/dist/next-public-fonts-local.d.ts +7 -0
  95. package/dist/next-public-fonts-local.js +64 -0
  96. package/dist/next-public-fonts.d.ts +13 -0
  97. package/dist/next-public-fonts.js +49 -0
  98. package/dist/next-source-hash.d.ts +14 -0
  99. package/dist/next-source-hash.js +18 -0
  100. package/dist/next-source.d.ts +35 -0
  101. package/dist/next-source.js +155 -0
  102. package/dist/next.d.ts +9 -0
  103. package/dist/next.js +5 -0
  104. package/dist/project.d.ts +3 -0
  105. package/dist/project.js +9 -1
  106. package/dist/runtime-contracts.d.ts +229 -0
  107. package/dist/runtime-contracts.js +224 -0
  108. package/dist/sdk.d.ts +2 -2
  109. package/dist/server/broker-contract.d.ts +68 -0
  110. package/dist/server/broker-contract.js +33 -0
  111. package/dist/server/client.d.ts +67 -0
  112. package/dist/server/client.js +159 -0
  113. package/dist/server/logger.d.ts +26 -0
  114. package/dist/server/logger.js +24 -0
  115. package/dist/server/release-forms.d.ts +13 -0
  116. package/dist/server/release-forms.js +124 -0
  117. package/dist/server/request-context.d.ts +74 -0
  118. package/dist/server/request-context.js +103 -0
  119. package/dist/server/runtime-config.d.ts +53 -0
  120. package/dist/server/runtime-config.js +64 -0
  121. package/dist/server.d.ts +8 -0
  122. package/dist/server.js +4 -0
  123. package/dist/templates/nextjs/.artifactignore +14 -0
  124. package/dist/templates/nextjs/README.md +151 -0
  125. package/dist/templates/nextjs/app/about/page.tsx +26 -0
  126. package/dist/templates/nextjs/app/actions.ts +18 -0
  127. package/dist/templates/nextjs/app/api/health/route.ts +6 -0
  128. package/dist/templates/nextjs/app/components/counter.tsx +19 -0
  129. package/dist/templates/nextjs/app/components/greeting-form.tsx +38 -0
  130. package/dist/templates/nextjs/app/globals.css +4 -0
  131. package/dist/templates/nextjs/app/layout.tsx +27 -0
  132. package/dist/templates/nextjs/app/page.tsx +51 -0
  133. package/dist/templates/nextjs/app/providers.tsx +107 -0
  134. package/dist/templates/nextjs/artifact.bundle.yml +55 -0
  135. package/dist/templates/nextjs/next.config.mjs +27 -0
  136. package/dist/templates/nextjs/package.json +32 -0
  137. package/dist/templates/nextjs/pnpm-workspace.yaml +8 -0
  138. package/dist/templates/nextjs/postcss.config.mjs +5 -0
  139. package/dist/templates/nextjs/tsconfig.json +20 -0
  140. package/dist/templates/react-vite/package.json +1 -1
  141. package/dist/tunnel/broker.d.ts +75 -0
  142. package/dist/tunnel/broker.js +451 -0
  143. package/dist/tunnel/client.d.ts +22 -0
  144. package/dist/tunnel/client.js +177 -0
  145. package/dist/tunnel/index.d.ts +6 -0
  146. package/dist/tunnel/index.js +4 -0
  147. package/dist/tunnel/io.d.ts +33 -0
  148. package/dist/tunnel/io.js +108 -0
  149. package/dist/tunnel/peer.d.ts +25 -0
  150. package/dist/tunnel/peer.js +147 -0
  151. package/dist/tunnel/protocol.d.ts +69 -0
  152. package/dist/tunnel/protocol.js +196 -0
  153. package/dist/tunnel/redis-ownership.d.ts +38 -0
  154. package/dist/tunnel/redis-ownership.js +100 -0
  155. package/dist/tunnel/redis-socket.d.ts +13 -0
  156. package/dist/tunnel/redis-socket.js +48 -0
  157. package/dist/tunnel/redis-types.d.ts +70 -0
  158. package/dist/tunnel/redis-types.js +9 -0
  159. package/dist/tunnel/redis.d.ts +53 -0
  160. package/dist/tunnel/redis.js +391 -0
  161. package/dist/tunnel/transport.d.ts +17 -0
  162. package/dist/tunnel/transport.js +1 -0
  163. package/package.json +67 -4
  164. package/templates/nextjs/.artifactignore +14 -0
  165. package/templates/nextjs/README.md +151 -0
  166. package/templates/nextjs/app/about/page.tsx +26 -0
  167. package/templates/nextjs/app/actions.ts +18 -0
  168. package/templates/nextjs/app/api/health/route.ts +6 -0
  169. package/templates/nextjs/app/components/counter.tsx +19 -0
  170. package/templates/nextjs/app/components/greeting-form.tsx +38 -0
  171. package/templates/nextjs/app/globals.css +4 -0
  172. package/templates/nextjs/app/layout.tsx +27 -0
  173. package/templates/nextjs/app/page.tsx +51 -0
  174. package/templates/nextjs/app/providers.tsx +107 -0
  175. package/templates/nextjs/artifact.bundle.yml +55 -0
  176. package/templates/nextjs/next.config.mjs +27 -0
  177. package/templates/nextjs/package.json +32 -0
  178. package/templates/nextjs/pnpm-workspace.yaml +8 -0
  179. package/templates/nextjs/postcss.config.mjs +5 -0
  180. package/templates/nextjs/tsconfig.json +20 -0
  181. package/templates/react-vite/package.json +1 -1
package/VERSION-PIN.md CHANGED
@@ -4,12 +4,19 @@ 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
+
13
20
  ## What this means
14
21
 
15
22
  `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
@@ -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,7 +34,7 @@ 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>
@@ -57,6 +58,12 @@ const USAGE = `usage:
57
58
  seq-studio login are still needed to resolve the repo and upload the
58
59
  deployment.)
59
60
 
61
+ Next.js: init --template nextjs, then pnpm install with Node.js 24. dev starts
62
+ a local Next server and an owner-only Atlas preview tunnel (no cloud credentials).
63
+ build --env creates an immutable hosted preview; deploy builds and publishes
64
+ a reviewed Git Service revision through Atlas. For local build checks, use pnpm build.
65
+ --once applies to static artifacts; --port applies to Next development.
66
+
60
67
  Authenticate with: seq-studio login`;
61
68
  export async function runCli(argv = process.argv.slice(2)) {
62
69
  if (argv.some((arg) => arg === '--token' || arg.startsWith('--token='))) {
@@ -118,17 +125,30 @@ async function initCommand(args) {
118
125
  const dir = resolve(args.positional[0] ?? '.');
119
126
  const name = dir.split('/').filter(Boolean).at(-1) ?? 'my-artifact';
120
127
  const slug = slugify(name);
128
+ const template = args.flags.template ?? 'react-vite';
129
+ if (template !== 'react-vite' && template !== 'nextjs') {
130
+ throw new Error('--template must be react-vite or nextjs.');
131
+ }
121
132
  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');
133
+ const templateRoot = existsSync(resolve(moduleDir, 'templates', template))
134
+ ? resolve(moduleDir, 'templates', template)
135
+ : resolve(moduleDir, '..', 'templates', template);
125
136
  await mkdir(dir, { recursive: true });
126
137
  await cp(templateRoot, dir, { recursive: true, force: false, errorOnExist: false });
127
138
  await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{slug\}\}/g, slug);
128
139
  await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{title\}\}/g, titleize(slug));
129
140
  await replaceInFile(join(dir, 'package.json'), /\{\{slug\}\}/g, slug);
141
+ if (template === 'nextjs') {
142
+ const peers = resolveArtifactStudioPeerVersions();
143
+ await replaceInFile(join(dir, 'package.json'), /\{\{artifactStudioVersion\}\}/g, peers.cliVersion);
144
+ for (const file of ['app/layout.tsx', 'app/page.tsx']) {
145
+ await replaceInFile(join(dir, file), /\{\{title\}\}/g, titleize(slug));
146
+ }
147
+ }
130
148
  console.log(`[seq-studio] initialized ${dir}`);
131
- console.log('Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
149
+ console.log(template === 'nextjs'
150
+ ? '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.'
151
+ : 'Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
132
152
  return 0;
133
153
  }
134
154
  async function whoamiCommand(args) {
@@ -184,10 +204,12 @@ async function linkCommand(args) {
184
204
  title: source.manifest.artifact.title,
185
205
  slug: source.manifest.artifact.project_id,
186
206
  description: source.manifest.artifact.description ?? null,
207
+ ...(isNextArtifactManifest(source.manifest) ? { projectType: 'nextjs' } : {}),
187
208
  },
188
209
  });
189
210
  project = created.project;
190
211
  }
212
+ assertProjectRuntime({ project, artifactType: source.manifest.artifact.type, declaredSlug: source.manifest.artifact.project_id });
191
213
  await writeLocalConfig({
192
214
  projectId: project.id,
193
215
  projectSlug: project.slug,
@@ -343,6 +365,12 @@ async function validateCommand(args) {
343
365
  console.log(`[seq-studio] valid ${source.manifest.bundle.name}`);
344
366
  console.log(`[seq-studio] files: ${source.files.length}`);
345
367
  console.log(`[seq-studio] source: ${source.sourceHash}`);
368
+ if (isNextArtifactManifest(source.manifest)) {
369
+ const { validateNextProject } = await import('./next-project.js');
370
+ await validateNextProject({ root: dir, manifest: source.manifest, installed: false });
371
+ console.log('[seq-studio] Next.js source is valid; Atlas performs the release build and hosted policy checks.');
372
+ return 0;
373
+ }
346
374
  reportSandboxWarnings(lintArtifactSandbox(source.files));
347
375
  return 0;
348
376
  }
@@ -372,6 +400,9 @@ async function buildCommand(args) {
372
400
  : {};
373
401
  const source = await resolveArtifactSource(spec, remote);
374
402
  try {
403
+ if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
404
+ return await buildNextCommand({ args, source, active: false });
405
+ }
375
406
  const result = await buildPreparedArtifactSource(source);
376
407
  reportBuildDiagnostics(result);
377
408
  // A remote source's dir is a temp dir we tear down below, so default the
@@ -402,6 +433,11 @@ async function planCommand(args) {
402
433
  token,
403
434
  });
404
435
  try {
436
+ const manifest = await readArtifactStudioManifest(source.dir);
437
+ if (isNextArtifactManifest(manifest)) {
438
+ const { planNextProject } = await import('./next-build.js');
439
+ return await planNextProject({ root: source.dir, manifest, environment: env.name });
440
+ }
405
441
  const result = await buildPreparedArtifactSource(source);
406
442
  reportBuildDiagnostics(result);
407
443
  console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
@@ -458,6 +494,9 @@ async function deployCommand(args) {
458
494
  // the current branch and dirty working tree.
459
495
  if (env.name !== 'local')
460
496
  assertActiveDeployProvenance(source.provenance);
497
+ if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
498
+ return await buildNextCommand({ args, source, active: true });
499
+ }
461
500
  // --skip-unchanged: hash the raw source (no build needed) and no-op when
462
501
  // the remote active deployment already matches *and* was built with the
463
502
  // same CLI/atlas-ui pin. Lets CI redeploy every artifact on every release
@@ -506,12 +545,56 @@ async function deployCommand(args) {
506
545
  await source.cleanup();
507
546
  }
508
547
  }
548
+ async function buildNextCommand({ args, source, active }) {
549
+ const env = await envFromArgs(args);
550
+ const token = await getRequiredToken(args);
551
+ const manifest = await readArtifactStudioManifest(source.dir);
552
+ if (!isNextArtifactManifest(manifest))
553
+ throw new Error('Expected a Next.js application manifest.');
554
+ const { nextSourceRepository, resolveNextRepositoryId, submitNextBuild } = await import('./next-build.js');
555
+ const spec = parseSourceSpec(args);
556
+ const repository = nextSourceRepository({ spec, root: source.dir });
557
+ if (active)
558
+ assertActiveDeployProvenance(source.provenance);
559
+ const repositoryId = active || spec.kind === 'git-service'
560
+ ? await resolveNextRepositoryId({ spec, repository, baseUrl: env.url, token })
561
+ : undefined;
562
+ const project = await ensureRemoteProject({
563
+ env: env.url, token, result: { manifest }, linkConfigDir: spec.kind === 'local' ? source.dir : null,
564
+ allowCreate: args.flags['no-create'] !== true,
565
+ explicitProjectId: typeof args.flags.project === 'string' ? args.flags.project : null,
566
+ });
567
+ if (args.flags['skip-unchanged']) {
568
+ console.log('[seq-studio] Next.js submits source to Atlas for build verification; local static-bundle skip checks do not apply.');
569
+ }
570
+ return submitNextBuild({
571
+ root: source.dir, manifest, projectId: project.id, baseUrl: env.url,
572
+ getToken: () => getRequiredToken(args), provenance: source.provenance, repository, repositoryId,
573
+ channel: active ? 'active' : 'preview', out: typeof args.flags.out === 'string' ? resolve(args.flags.out) : undefined,
574
+ });
575
+ }
509
576
  async function devCommand(args) {
510
577
  if (args.flags.repo || args.flags['git-url']) {
511
578
  throw new Error('dev watches a local folder; --repo / --git-url are only for build, plan, and deploy.');
512
579
  }
513
580
  const dir = resolve(args.positional[0] ?? '.');
514
581
  const env = await envFromArgs(args);
582
+ const initialManifest = await readArtifactStudioManifest(dir);
583
+ if (isNextArtifactManifest(initialManifest)) {
584
+ if (args.flags.once)
585
+ throw new Error('Next.js dev is a live local session. Use artifact build --env <env> for an immutable hosted preview.');
586
+ const auth = await getRequiredAuth(args);
587
+ if (auth.authMode !== 'user')
588
+ throw new Error('Local Next.js development requires a user login. Run seq-studio login.');
589
+ const project = await ensureRemoteProject({ env: env.url, token: auth.token, result: { manifest: initialManifest }, linkConfigDir: dir });
590
+ const { runNextDevelopment } = await import('./next-dev.js');
591
+ return runNextDevelopment({
592
+ root: dir, manifest: initialManifest, projectId: project.id, baseUrl: env.url,
593
+ environment: env.name, previewKey: typeof args.flags.key === 'string' ? args.flags.key : 'default',
594
+ port: typeof args.flags.port === 'string' ? args.flags.port : undefined,
595
+ links: parseLinks(args.flags.link), getToken: () => getRequiredToken(args),
596
+ });
597
+ }
515
598
  // Validate auth up front for a fast failure, but resolve a fresh token on
516
599
  // every push (below) so a long-running watcher survives access-token expiry
517
600
  // instead of dying with "Authentication failed" once the initial token's TTL
@@ -725,9 +808,9 @@ async function pullCommand(args) {
725
808
  path: `/api/artifact-studio/projects/${projectId}/deployments/${projectData.project.activeDeploymentId}/files`,
726
809
  });
727
810
  for (const file of filesData.files) {
728
- const target = join(out, file.path);
811
+ const target = join(out, normalizeArtifactPath(file.path));
729
812
  await mkdir(dirname(target), { recursive: true });
730
- await writeFile(target, file.content, 'utf8');
813
+ await writeFile(target, file.encoding === 'base64' ? Buffer.from(file.content, 'base64') : file.content);
731
814
  }
732
815
  console.log(`[seq-studio] pulled ${projectId} to ${out}`);
733
816
  return 0;
@@ -774,18 +857,21 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
774
857
  throw new Error(`--project ${explicitProjectId} resolves to slug "${data.project.slug}" but this source's ` +
775
858
  `artifact.bundle.yml declares project_id "${result.manifest.artifact.project_id}" — refusing to deploy over a different project`);
776
859
  }
860
+ assertProjectRuntime({ project: data.project, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
777
861
  return data.project;
778
862
  }
779
863
  const config = linkConfigDir ? await readLocalConfig(linkConfigDir) : {};
780
864
  if (config.projectId) {
781
865
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
782
866
  if (data) {
867
+ assertProjectRuntime({ project: data.project, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
783
868
  return data.project;
784
869
  }
785
870
  console.warn(`[seq-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
786
871
  }
787
872
  const existing = await fetchRemoteProject({ env, token, slug: result.manifest.artifact.project_id });
788
873
  if (existing) {
874
+ assertProjectRuntime({ project: existing, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
789
875
  if (linkConfigDir)
790
876
  await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, linkConfigDir);
791
877
  return existing;
@@ -803,12 +889,21 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
803
889
  title: result.manifest.artifact.title,
804
890
  slug: result.manifest.artifact.project_id,
805
891
  description: result.manifest.artifact.description ?? null,
892
+ ...(result.manifest.artifact.type === 'nextjs' ? { projectType: 'nextjs' } : {}),
806
893
  },
807
894
  });
808
895
  if (linkConfigDir)
809
896
  await writeLocalConfig({ ...config, projectId: created.project.id, projectSlug: created.project.slug }, linkConfigDir);
810
897
  return created.project;
811
898
  }
899
+ function assertProjectRuntime({ project, artifactType, declaredSlug }) {
900
+ if (artifactType === 'nextjs' && project.slug !== declaredSlug) {
901
+ throw new Error(`Linked project ${project.slug} does not match artifact.bundle.yml project_id ${declaredSlug}. Relink to the intended project.`);
902
+ }
903
+ if (artifactType === 'nextjs' && project.projectType && project.projectType !== 'nextjs') {
904
+ 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.`);
905
+ }
906
+ }
812
907
  async function uploadDeployment({ env, token, projectId, result, channel, previewKey, linked = false, provenance, message, }) {
813
908
  // Active deploys omit `version`: the server derives it from its own
814
909
  // sourceHash and de-dupes against the (projectId, version) unique
@@ -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,55 @@
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
+ constructor(status: 429 | 503, code: 'capacity_exceeded' | 'admission_unavailable');
38
+ }
39
+ /** Fail-closed admission with no command retries. An uncertain acquisition is
40
+ * never used; its unique permit expires even if Redis accepted it before loss. */
41
+ export declare class ArtifactRequestAdmission {
42
+ private readonly options;
43
+ private readonly namespace;
44
+ private readonly ceiling;
45
+ constructor(options: {
46
+ redis: AdmissionRedis;
47
+ namespace: string;
48
+ platformCeiling?: number;
49
+ onError?: (event: {
50
+ kind: 'admission_release_failed' | 'admission_timeout_callback_failed';
51
+ }) => void;
52
+ });
53
+ acquire(target: AdmissionTarget): Promise<AdmissionLease>;
54
+ }
55
+ export {};
@@ -0,0 +1,132 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { performance } from 'node:perf_hooks';
3
+ import { z } from 'zod';
4
+ import { artifactNextResourceLimitsSchema } from '../manifest-next.js';
5
+ import { redisNamespaceSchema } from '../tunnel/redis-ownership.js';
6
+ export class ArtifactAdmissionError extends Error {
7
+ status;
8
+ code;
9
+ constructor(status, code) {
10
+ super(status === 429 ? 'Application request capacity exceeded' : 'Application request admission unavailable');
11
+ this.status = status;
12
+ this.code = code;
13
+ this.name = 'ArtifactAdmissionError';
14
+ }
15
+ }
16
+ // The tunnel's APP_ENV namespace format, so both Redis key spaces accept the same deployment identities.
17
+ const namespace = redisNamespaceSchema;
18
+ const identifier = z.string().min(1).max(128).regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/);
19
+ const resourceFields = artifactNextResourceLimitsSchema.unwrap().shape;
20
+ const resources = z.object({
21
+ concurrency: resourceFields.concurrency,
22
+ max_instances: resourceFields.max_instances,
23
+ timeout_seconds: resourceFields.timeout_seconds,
24
+ });
25
+ // Both keys have the same Redis Cluster hash tag. A lease can never exist in
26
+ // only one set, and previews/retained releases consume the same application cap.
27
+ const ACQUIRE = `-- artifact-request-admission-acquire-v1
28
+ local time = redis.call('TIME')
29
+ local now = tonumber(time[1]) * 1000 + math.floor(tonumber(time[2]) / 1000)
30
+ local maximum = tonumber(ARGV[2])
31
+ local deadline = now + tonumber(ARGV[3])
32
+ for _, key in ipairs(KEYS) do
33
+ redis.call('ZREMRANGEBYSCORE', key, '-inf', now)
34
+ if redis.call('ZCARD', key) >= maximum then return {0, now} end
35
+ end
36
+ for _, key in ipairs(KEYS) do
37
+ redis.call('ZADD', key, deadline, ARGV[1])
38
+ local latest = redis.call('ZREVRANGE', key, 0, 0, 'WITHSCORES')
39
+ redis.call('PEXPIRE', key, tonumber(latest[2]) - now + 1000)
40
+ end
41
+ return {1, deadline}`;
42
+ const RELEASE = `-- artifact-request-admission-release-v1
43
+ for _, key in ipairs(KEYS) do
44
+ redis.call('ZREM', key, ARGV[1])
45
+ if redis.call('ZCARD', key) == 0 then redis.call('DEL', key) end
46
+ end
47
+ return 1`;
48
+ /** Fail-closed admission with no command retries. An uncertain acquisition is
49
+ * never used; its unique permit expires even if Redis accepted it before loss. */
50
+ export class ArtifactRequestAdmission {
51
+ options;
52
+ namespace;
53
+ ceiling;
54
+ constructor(options) {
55
+ this.options = options;
56
+ this.namespace = namespace.parse(options.namespace);
57
+ this.ceiling = z.number().int().min(1).max(256).parse(options.platformCeiling ?? 256);
58
+ const policy = options.redis.options;
59
+ if (policy.enableOfflineQueue !== false || policy.autoResendUnfulfilledCommands !== false || policy.maxRetriesPerRequest !== 0) {
60
+ throw new Error('Admission Redis must disable offline queues, command replay and retries');
61
+ }
62
+ }
63
+ async acquire(target) {
64
+ if (namespace.parse(target.environment) !== this.namespace)
65
+ throw new Error('Admission environment mismatch');
66
+ const projectId = identifier.parse(target.projectId), deploymentId = identifier.parse(target.deploymentId);
67
+ const limits = resources.parse(target.resources ?? {});
68
+ const maximum = Math.min(limits.concurrency * limits.max_instances, this.ceiling);
69
+ const timeoutMs = limits.timeout_seconds * 1000;
70
+ const requestId = randomUUID();
71
+ const prefix = `artifact-admission:${this.namespace}:{${this.namespace}:${projectId}}`;
72
+ const keys = [`${prefix}:app`, `${prefix}:deployment:${deploymentId}`];
73
+ const began = performance.now();
74
+ let result;
75
+ try {
76
+ result = z.tuple([z.number().int().min(0).max(1), z.number().int().positive()]).parse(await this.options.redis.eval(ACQUIRE, 2, ...keys, requestId, maximum, timeoutMs));
77
+ }
78
+ catch {
79
+ throw new ArtifactAdmissionError(503, 'admission_unavailable');
80
+ }
81
+ if (result[0] === 0)
82
+ throw new ArtifactAdmissionError(429, 'capacity_exceeded');
83
+ // Never start work under an admission whose local request budget already
84
+ // elapsed while Redis was unavailable or stalled. Redis TTL reclaims it.
85
+ if (performance.now() - began >= timeoutMs)
86
+ throw new ArtifactAdmissionError(503, 'admission_unavailable');
87
+ let releasePromise;
88
+ let cleanup;
89
+ let bound = false;
90
+ const release = () => {
91
+ cleanup?.();
92
+ // Repeated finish/close/error notifications must never free another
93
+ // request's permit, nor cause a failed command to be retried.
94
+ releasePromise ??= this.options.redis.eval(RELEASE, 2, ...keys, requestId).then(() => undefined, () => { throw new ArtifactAdmissionError(503, 'admission_unavailable'); });
95
+ return releasePromise;
96
+ };
97
+ const releaseQuietly = () => {
98
+ void release().catch(() => this.options.onError?.({ kind: 'admission_release_failed' }));
99
+ };
100
+ return {
101
+ requestId, deadline: result[1], timeoutMs, release,
102
+ bind: (response, onTimeout) => {
103
+ if (bound || releasePromise)
104
+ throw new Error('Admission lease already bound or released');
105
+ bound = true;
106
+ const timer = setTimeout(() => {
107
+ try {
108
+ onTimeout();
109
+ }
110
+ catch {
111
+ this.options.onError?.({ kind: 'admission_timeout_callback_failed' });
112
+ }
113
+ finally {
114
+ releaseQuietly();
115
+ }
116
+ }, Math.max(0, timeoutMs - (performance.now() - began)));
117
+ timer.unref();
118
+ cleanup = () => {
119
+ clearTimeout(timer);
120
+ response.off('finish', releaseQuietly);
121
+ response.off('close', releaseQuietly);
122
+ };
123
+ response.once('finish', releaseQuietly);
124
+ response.once('close', releaseQuietly);
125
+ if (response.writableFinished || response.destroyed)
126
+ releaseQuietly();
127
+ // Detaching also releases; callers cannot strand an admitted permit.
128
+ return releaseQuietly;
129
+ },
130
+ };
131
+ }
132
+ }