@sequenceholdings/artifact-studio 0.2.5 → 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 (187) hide show
  1. package/README.md +35 -0
  2. package/VERSION-PIN.md +14 -3
  3. package/dist/api.d.ts +11 -1
  4. package/dist/api.js +26 -9
  5. package/dist/atlas-ui-css-plugin.d.ts +11 -7
  6. package/dist/atlas-ui-css-plugin.js +38 -10
  7. package/dist/build.d.ts +2 -2
  8. package/dist/build.js +14 -6
  9. package/dist/cli.d.ts +1 -1
  10. package/dist/cli.js +108 -37
  11. package/dist/dev-link.d.ts +1 -1
  12. package/dist/dev-link.js +2 -1
  13. package/dist/gateway/admission.d.ts +55 -0
  14. package/dist/gateway/admission.js +132 -0
  15. package/dist/gateway/control.d.ts +91 -0
  16. package/dist/gateway/control.js +113 -0
  17. package/dist/gateway/hosted-proxy.d.ts +36 -0
  18. package/dist/gateway/hosted-proxy.js +347 -0
  19. package/dist/gateway/release-admission.d.ts +23 -0
  20. package/dist/gateway/release-admission.js +259 -0
  21. package/dist/gateway/server.d.ts +29 -0
  22. package/dist/gateway/server.js +298 -0
  23. package/dist/gateway/session.d.ts +71 -0
  24. package/dist/gateway/session.js +143 -0
  25. package/dist/gateway/usage.d.ts +37 -0
  26. package/dist/gateway/usage.js +46 -0
  27. package/dist/gateway-main.d.ts +1 -0
  28. package/dist/gateway-main.js +84 -0
  29. package/dist/git-clone.d.ts +23 -1
  30. package/dist/git-clone.js +32 -15
  31. package/dist/manifest-next.d.ts +217 -0
  32. package/dist/manifest-next.js +120 -0
  33. package/dist/manifest-static.d.ts +76 -0
  34. package/dist/manifest-static.js +124 -0
  35. package/dist/manifest.d.ts +124 -19
  36. package/dist/manifest.js +12 -118
  37. package/dist/next/bridge.d.ts +47 -0
  38. package/dist/next/bridge.js +229 -0
  39. package/dist/next/browser-logs.d.ts +7 -0
  40. package/dist/next/browser-logs.js +51 -0
  41. package/dist/next/browser.d.ts +6 -0
  42. package/dist/next/browser.js +57 -0
  43. package/dist/next/protocol.d.ts +73 -0
  44. package/dist/next/protocol.js +56 -0
  45. package/dist/next/release.d.ts +15 -0
  46. package/dist/next/release.js +46 -0
  47. package/dist/next/requests.d.ts +15 -0
  48. package/dist/next/requests.js +59 -0
  49. package/dist/next/routes.d.ts +4 -0
  50. package/dist/next/routes.js +21 -0
  51. package/dist/next/streams.d.ts +16 -0
  52. package/dist/next/streams.js +132 -0
  53. package/dist/next/theme.d.ts +10 -0
  54. package/dist/next/theme.js +45 -0
  55. package/dist/next/transport.d.ts +15 -0
  56. package/dist/next/transport.js +13 -0
  57. package/dist/next-build.d.ts +29 -0
  58. package/dist/next-build.js +129 -0
  59. package/dist/next-cache/adapter-runtime.d.ts +51 -0
  60. package/dist/next-cache/adapter-runtime.js +73 -0
  61. package/dist/next-cache/component-handler.d.ts +3 -0
  62. package/dist/next-cache/component-handler.js +10 -0
  63. package/dist/next-cache/components.d.ts +20 -0
  64. package/dist/next-cache/components.js +113 -0
  65. package/dist/next-cache/config.d.ts +9 -0
  66. package/dist/next-cache/config.js +78 -0
  67. package/dist/next-cache/incremental-handler.d.ts +18 -0
  68. package/dist/next-cache/incremental-handler.js +17 -0
  69. package/dist/next-cache/incremental.d.ts +27 -0
  70. package/dist/next-cache/incremental.js +99 -0
  71. package/dist/next-cache/index.d.ts +18 -0
  72. package/dist/next-cache/index.js +16 -0
  73. package/dist/next-cache/memory-store.d.ts +27 -0
  74. package/dist/next-cache/memory-store.js +118 -0
  75. package/dist/next-cache/redis-scripts.d.ts +3 -0
  76. package/dist/next-cache/redis-scripts.js +85 -0
  77. package/dist/next-cache/redis-store.d.ts +18 -0
  78. package/dist/next-cache/redis-store.js +56 -0
  79. package/dist/next-cache/runtime.d.ts +29 -0
  80. package/dist/next-cache/runtime.js +63 -0
  81. package/dist/next-cache/serialization.d.ts +39 -0
  82. package/dist/next-cache/serialization.js +333 -0
  83. package/dist/next-cache/store.d.ts +54 -0
  84. package/dist/next-cache/store.js +45 -0
  85. package/dist/next-cache/valkey-transport.d.ts +52 -0
  86. package/dist/next-cache/valkey-transport.js +189 -0
  87. package/dist/next-dev-control.d.ts +48 -0
  88. package/dist/next-dev-control.js +63 -0
  89. package/dist/next-dev-process.d.ts +17 -0
  90. package/dist/next-dev-process.js +110 -0
  91. package/dist/next-dev.d.ts +12 -0
  92. package/dist/next-dev.js +267 -0
  93. package/dist/next-image-config.d.ts +9 -0
  94. package/dist/next-image-config.js +101 -0
  95. package/dist/next-project.d.ts +22 -0
  96. package/dist/next-project.js +64 -0
  97. package/dist/next-public-fonts-local.d.ts +7 -0
  98. package/dist/next-public-fonts-local.js +64 -0
  99. package/dist/next-public-fonts.d.ts +13 -0
  100. package/dist/next-public-fonts.js +49 -0
  101. package/dist/next-source-hash.d.ts +14 -0
  102. package/dist/next-source-hash.js +18 -0
  103. package/dist/next-source.d.ts +35 -0
  104. package/dist/next-source.js +155 -0
  105. package/dist/next.d.ts +9 -0
  106. package/dist/next.js +5 -0
  107. package/dist/project.d.ts +3 -0
  108. package/dist/project.js +9 -1
  109. package/dist/runtime-contracts.d.ts +229 -0
  110. package/dist/runtime-contracts.js +224 -0
  111. package/dist/sdk.d.ts +420 -1
  112. package/dist/sdk.js +35 -0
  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 +67 -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 +74 -0
  122. package/dist/server/request-context.js +103 -0
  123. package/dist/server/runtime-config.d.ts +53 -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 +151 -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/next.config.mjs +27 -0
  140. package/dist/templates/nextjs/package.json +32 -0
  141. package/dist/templates/nextjs/pnpm-workspace.yaml +8 -0
  142. package/dist/templates/nextjs/postcss.config.mjs +5 -0
  143. package/dist/templates/nextjs/tsconfig.json +20 -0
  144. package/dist/templates/react-vite/artifact.bundle.yml +0 -1
  145. package/dist/templates/react-vite/package.json +1 -1
  146. package/dist/tunnel/broker.d.ts +75 -0
  147. package/dist/tunnel/broker.js +451 -0
  148. package/dist/tunnel/client.d.ts +22 -0
  149. package/dist/tunnel/client.js +177 -0
  150. package/dist/tunnel/index.d.ts +6 -0
  151. package/dist/tunnel/index.js +4 -0
  152. package/dist/tunnel/io.d.ts +33 -0
  153. package/dist/tunnel/io.js +108 -0
  154. package/dist/tunnel/peer.d.ts +25 -0
  155. package/dist/tunnel/peer.js +147 -0
  156. package/dist/tunnel/protocol.d.ts +69 -0
  157. package/dist/tunnel/protocol.js +196 -0
  158. package/dist/tunnel/redis-ownership.d.ts +38 -0
  159. package/dist/tunnel/redis-ownership.js +100 -0
  160. package/dist/tunnel/redis-socket.d.ts +13 -0
  161. package/dist/tunnel/redis-socket.js +48 -0
  162. package/dist/tunnel/redis-types.d.ts +70 -0
  163. package/dist/tunnel/redis-types.js +9 -0
  164. package/dist/tunnel/redis.d.ts +53 -0
  165. package/dist/tunnel/redis.js +391 -0
  166. package/dist/tunnel/transport.d.ts +17 -0
  167. package/dist/tunnel/transport.js +1 -0
  168. package/package.json +68 -4
  169. package/templates/nextjs/.artifactignore +14 -0
  170. package/templates/nextjs/README.md +151 -0
  171. package/templates/nextjs/app/about/page.tsx +26 -0
  172. package/templates/nextjs/app/actions.ts +18 -0
  173. package/templates/nextjs/app/api/health/route.ts +6 -0
  174. package/templates/nextjs/app/components/counter.tsx +19 -0
  175. package/templates/nextjs/app/components/greeting-form.tsx +38 -0
  176. package/templates/nextjs/app/globals.css +4 -0
  177. package/templates/nextjs/app/layout.tsx +27 -0
  178. package/templates/nextjs/app/page.tsx +51 -0
  179. package/templates/nextjs/app/providers.tsx +107 -0
  180. package/templates/nextjs/artifact.bundle.yml +55 -0
  181. package/templates/nextjs/next.config.mjs +27 -0
  182. package/templates/nextjs/package.json +32 -0
  183. package/templates/nextjs/pnpm-workspace.yaml +8 -0
  184. package/templates/nextjs/postcss.config.mjs +5 -0
  185. package/templates/nextjs/tsconfig.json +20 -0
  186. package/templates/react-vite/artifact.bundle.yml +0 -1
  187. package/templates/react-vite/package.json +1 -1
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,
@@ -254,8 +276,7 @@ async function listCommand(args) {
254
276
  : project.activeDeploymentId
255
277
  ? 'deployed'
256
278
  : 'not deployed';
257
- const visibility = project.visibility ? ` ${project.visibility}` : '';
258
- console.log(` ${project.slug} id=${project.id} [${active}]${visibility} ${project.title}`);
279
+ console.log(` ${project.slug} id=${project.id} [${active}] ${project.title}`);
259
280
  }
260
281
  return 0;
261
282
  }
@@ -300,8 +321,6 @@ async function showCommand(args) {
300
321
  console.log(` title: ${project.title}`);
301
322
  if (project.description)
302
323
  console.log(` description: ${project.description}`);
303
- if (project.visibility)
304
- console.log(` visibility: ${project.visibility}`);
305
324
  if (project.appEligible !== undefined)
306
325
  console.log(` appEligible: ${project.appEligible ? 'yes' : 'no'}`);
307
326
  if (project.createdBy)
@@ -346,6 +365,12 @@ async function validateCommand(args) {
346
365
  console.log(`[seq-studio] valid ${source.manifest.bundle.name}`);
347
366
  console.log(`[seq-studio] files: ${source.files.length}`);
348
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
+ }
349
374
  reportSandboxWarnings(lintArtifactSandbox(source.files));
350
375
  return 0;
351
376
  }
@@ -375,6 +400,9 @@ async function buildCommand(args) {
375
400
  : {};
376
401
  const source = await resolveArtifactSource(spec, remote);
377
402
  try {
403
+ if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
404
+ return await buildNextCommand({ args, source, active: false });
405
+ }
378
406
  const result = await buildPreparedArtifactSource(source);
379
407
  reportBuildDiagnostics(result);
380
408
  // A remote source's dir is a temp dir we tear down below, so default the
@@ -405,6 +433,11 @@ async function planCommand(args) {
405
433
  token,
406
434
  });
407
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
+ }
408
441
  const result = await buildPreparedArtifactSource(source);
409
442
  reportBuildDiagnostics(result);
410
443
  console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
@@ -461,6 +494,9 @@ async function deployCommand(args) {
461
494
  // the current branch and dirty working tree.
462
495
  if (env.name !== 'local')
463
496
  assertActiveDeployProvenance(source.provenance);
497
+ if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
498
+ return await buildNextCommand({ args, source, active: true });
499
+ }
464
500
  // --skip-unchanged: hash the raw source (no build needed) and no-op when
465
501
  // the remote active deployment already matches *and* was built with the
466
502
  // same CLI/atlas-ui pin. Lets CI redeploy every artifact on every release
@@ -499,7 +535,6 @@ async function deployCommand(args) {
499
535
  linkConfigDir: spec.kind === 'local' ? source.dir : null,
500
536
  allowCreate: args.flags['no-create'] !== true,
501
537
  explicitProjectId,
502
- envName: env.name,
503
538
  });
504
539
  const deployment = await uploadDeployment({ env: env.url, token, projectId: project.id, result, channel: 'active', provenance: source.provenance });
505
540
  console.log(`[seq-studio] deployed ${project.title} ${deployment.version}`);
@@ -510,12 +545,56 @@ async function deployCommand(args) {
510
545
  await source.cleanup();
511
546
  }
512
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
+ }
513
576
  async function devCommand(args) {
514
577
  if (args.flags.repo || args.flags['git-url']) {
515
578
  throw new Error('dev watches a local folder; --repo / --git-url are only for build, plan, and deploy.');
516
579
  }
517
580
  const dir = resolve(args.positional[0] ?? '.');
518
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
+ }
519
598
  // Validate auth up front for a fast failure, but resolve a fresh token on
520
599
  // every push (below) so a long-running watcher survives access-token expiry
521
600
  // instead of dying with "Authentication failed" once the initial token's TTL
@@ -589,9 +668,7 @@ async function devCommand(args) {
589
668
  // Resolve per push so the token refreshes across a long watch session.
590
669
  const token = await getRequiredToken(args);
591
670
  if (!projectId) {
592
- // envName so a first-time `artifact dev` creation honors the manifest
593
- // visibility too (and notes drift, once, when the project pre-exists).
594
- const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir, envName: env.name });
671
+ const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir });
595
672
  projectId = project.id;
596
673
  }
597
674
  const deployment = await uploadDeployment({
@@ -731,9 +808,9 @@ async function pullCommand(args) {
731
808
  path: `/api/artifact-studio/projects/${projectId}/deployments/${projectData.project.activeDeploymentId}/files`,
732
809
  });
733
810
  for (const file of filesData.files) {
734
- const target = join(out, file.path);
811
+ const target = join(out, normalizeArtifactPath(file.path));
735
812
  await mkdir(dirname(target), { recursive: true });
736
- await writeFile(target, file.content, 'utf8');
813
+ await writeFile(target, file.encoding === 'base64' ? Buffer.from(file.content, 'base64') : file.content);
737
814
  }
738
815
  console.log(`[seq-studio] pulled ${projectId} to ${out}`);
739
816
  return 0;
@@ -771,21 +848,7 @@ export function activeDeploymentMatchesSkipPin({ active, sourceHash, peerVersion
771
848
  return (active.cliVersion === peerVersions.cliVersion &&
772
849
  active.atlasUiVersion === peerVersions.atlasUiVersion);
773
850
  }
774
- /**
775
- * Deploys never change a live project's visibility — the Studio sharing UI /
776
- * PATCH own that. The manifest's targets.<env>.visibility is honored at
777
- * project CREATION only; on drift, say so instead of silently ignoring the
778
- * field (which is how it read as decorative for a year).
779
- */
780
- function warnOnVisibilityDrift({ project, declared, envName }) {
781
- if (!declared || !envName || !project.visibility || declared === project.visibility)
782
- return;
783
- console.warn(`[seq-studio] note: artifact.bundle.yml targets.${envName}.visibility is "${declared}" but the project is ` +
784
- `"${project.visibility}" on this environment — deploys never change visibility. Update it in Studio sharing ` +
785
- `(or PATCH /api/artifact-studio/projects/${project.id}) if the manifest is what you intend.`);
786
- }
787
- async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null, envName = null }) {
788
- const declaredVisibility = envName ? result.manifest.targets?.[envName]?.visibility : undefined;
851
+ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null }) {
789
852
  if (explicitProjectId) {
790
853
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${explicitProjectId}` });
791
854
  if (!data)
@@ -794,23 +857,23 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
794
857
  throw new Error(`--project ${explicitProjectId} resolves to slug "${data.project.slug}" but this source's ` +
795
858
  `artifact.bundle.yml declares project_id "${result.manifest.artifact.project_id}" — refusing to deploy over a different project`);
796
859
  }
797
- warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
860
+ assertProjectRuntime({ project: data.project, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
798
861
  return data.project;
799
862
  }
800
863
  const config = linkConfigDir ? await readLocalConfig(linkConfigDir) : {};
801
864
  if (config.projectId) {
802
865
  const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
803
866
  if (data) {
804
- warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
867
+ assertProjectRuntime({ project: data.project, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
805
868
  return data.project;
806
869
  }
807
870
  console.warn(`[seq-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
808
871
  }
809
872
  const existing = await fetchRemoteProject({ env, token, slug: result.manifest.artifact.project_id });
810
873
  if (existing) {
874
+ assertProjectRuntime({ project: existing, artifactType: result.manifest.artifact.type, declaredSlug: result.manifest.artifact.project_id });
811
875
  if (linkConfigDir)
812
876
  await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, linkConfigDir);
813
- warnOnVisibilityDrift({ project: existing, declared: declaredVisibility, envName });
814
877
  return existing;
815
878
  }
816
879
  if (!allowCreate) {
@@ -826,13 +889,21 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
826
889
  title: result.manifest.artifact.title,
827
890
  slug: result.manifest.artifact.project_id,
828
891
  description: result.manifest.artifact.description ?? null,
829
- ...(declaredVisibility ? { visibility: declaredVisibility } : {}),
892
+ ...(result.manifest.artifact.type === 'nextjs' ? { projectType: 'nextjs' } : {}),
830
893
  },
831
894
  });
832
895
  if (linkConfigDir)
833
896
  await writeLocalConfig({ ...config, projectId: created.project.id, projectSlug: created.project.slug }, linkConfigDir);
834
897
  return created.project;
835
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
+ }
836
907
  async function uploadDeployment({ env, token, projectId, result, channel, previewKey, linked = false, provenance, message, }) {
837
908
  // Active deploys omit `version`: the server derives it from its own
838
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
+ }
@@ -0,0 +1,91 @@
1
+ import { z } from 'zod';
2
+ import type { TunnelLease } from '../tunnel/index.js';
3
+ import type { GatewaySession } from './session.js';
4
+ import type { GatewayUsageEvent } from './usage.js';
5
+ export declare const gatewayTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
6
+ kind: z.ZodLiteral<"development">;
7
+ sessionId: z.ZodString;
8
+ epoch: z.ZodNumber;
9
+ projectId: z.ZodString;
10
+ ownerId: z.ZodString;
11
+ environment: z.ZodString;
12
+ publicOrigin: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
13
+ expiresAt: z.ZodNumber;
14
+ maxExpiresAt: z.ZodOptional<z.ZodNumber>;
15
+ capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
16
+ serviceAccountId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
17
+ lattice: z.ZodOptional<z.ZodObject<{
18
+ runId: z.ZodNullable<z.ZodString>;
19
+ nodeId: z.ZodNullable<z.ZodString>;
20
+ stepId: z.ZodNullable<z.ZodString>;
21
+ processId: z.ZodNullable<z.ZodString>;
22
+ }, z.core.$strict>>;
23
+ taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
24
+ manifest: z.ZodOptional<z.ZodObject<{
25
+ server: z.ZodObject<{
26
+ resources: z.ZodObject<{
27
+ concurrency: z.ZodNumber;
28
+ max_instances: z.ZodNumber;
29
+ timeout_seconds: z.ZodNumber;
30
+ }, z.core.$strip>;
31
+ }, z.core.$strip>;
32
+ }, z.core.$strip>>;
33
+ }, z.core.$strip>, z.ZodObject<{
34
+ kind: z.ZodLiteral<"hosted">;
35
+ deploymentId: z.ZodString;
36
+ frameworkBuildId: z.ZodString;
37
+ upstreamUrl: z.ZodString;
38
+ upstreamAudience: z.ZodOptional<z.ZodString>;
39
+ active: z.ZodBoolean;
40
+ published: z.ZodOptional<z.ZodBoolean>;
41
+ projectId: z.ZodString;
42
+ ownerId: z.ZodString;
43
+ environment: z.ZodString;
44
+ publicOrigin: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
45
+ expiresAt: z.ZodNumber;
46
+ maxExpiresAt: z.ZodOptional<z.ZodNumber>;
47
+ capabilities: z.ZodDefault<z.ZodArray<z.ZodString>>;
48
+ serviceAccountId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
49
+ lattice: z.ZodOptional<z.ZodObject<{
50
+ runId: z.ZodNullable<z.ZodString>;
51
+ nodeId: z.ZodNullable<z.ZodString>;
52
+ stepId: z.ZodNullable<z.ZodString>;
53
+ processId: z.ZodNullable<z.ZodString>;
54
+ }, z.core.$strict>>;
55
+ taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
56
+ manifest: z.ZodOptional<z.ZodObject<{
57
+ server: z.ZodObject<{
58
+ resources: z.ZodObject<{
59
+ concurrency: z.ZodNumber;
60
+ max_instances: z.ZodNumber;
61
+ timeout_seconds: z.ZodNumber;
62
+ }, z.core.$strip>;
63
+ }, z.core.$strip>;
64
+ }, z.core.$strip>>;
65
+ }, z.core.$strip>], "kind">;
66
+ export type GatewayTarget = z.infer<typeof gatewayTargetSchema>;
67
+ export declare class GatewayControlError extends Error {
68
+ readonly status: number;
69
+ constructor(status: number);
70
+ }
71
+ export declare class ArtifactGatewayControl {
72
+ private readonly options;
73
+ private readonly origin;
74
+ constructor(options: {
75
+ url: string;
76
+ token: string;
77
+ fetch?: typeof fetch;
78
+ });
79
+ private call;
80
+ usage(event: GatewayUsageEvent): Promise<void>;
81
+ bootstrap(ticket: string): Promise<{
82
+ session: GatewaySession;
83
+ target: GatewayTarget;
84
+ }>;
85
+ authorize(session: GatewaySession): Promise<GatewayTarget>;
86
+ tunnel({ token, sessionId, epoch }: {
87
+ token: string;
88
+ sessionId: string;
89
+ epoch: number;
90
+ }): Promise<TunnelLease>;
91
+ }