@sequenceholdings/artifact-studio 0.4.0 → 0.6.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 (114) hide show
  1. package/README.md +35 -1
  2. package/VERSION-PIN.md +29 -0
  3. package/dist/active-deploy-policy.d.ts +9 -3
  4. package/dist/active-deploy-policy.js +20 -8
  5. package/dist/artifact-env-bindings.d.ts +9 -0
  6. package/dist/artifact-env-bindings.js +32 -0
  7. package/dist/build.js +6 -0
  8. package/dist/cli.js +62 -13
  9. package/dist/deployment-validation.d.ts +2 -0
  10. package/dist/deployment-validation.js +2 -0
  11. package/dist/dev-link.js +28 -2
  12. package/dist/env-binding-scan.d.ts +10 -0
  13. package/dist/env-binding-scan.js +179 -0
  14. package/dist/env-bindings.d.ts +52 -0
  15. package/dist/env-bindings.js +180 -0
  16. package/dist/gateway/control.d.ts +33 -0
  17. package/dist/gateway/control.js +29 -0
  18. package/dist/gateway/hosted-proxy.d.ts +5 -0
  19. package/dist/gateway/hosted-proxy.js +4 -1
  20. package/dist/gateway/lattice-invoke.d.ts +48 -0
  21. package/dist/gateway/lattice-invoke.js +245 -0
  22. package/dist/gateway/lattice-upstream.d.ts +38 -0
  23. package/dist/gateway/lattice-upstream.js +136 -0
  24. package/dist/gateway/server.js +2 -3
  25. package/dist/gateway/session.d.ts +3 -1
  26. package/dist/gateway/session.js +10 -3
  27. package/dist/manifest-next.d.ts +55 -0
  28. package/dist/manifest-next.js +35 -2
  29. package/dist/manifest-static.d.ts +35 -0
  30. package/dist/manifest-static.js +12 -0
  31. package/dist/manifest.d.ts +48 -0
  32. package/dist/next-build.d.ts +2 -1
  33. package/dist/next-build.js +12 -7
  34. package/dist/offline/cache.d.ts +4 -0
  35. package/dist/offline/cache.js +7 -0
  36. package/dist/offline/connector.d.ts +28 -0
  37. package/dist/offline/connector.js +122 -0
  38. package/dist/offline/crypto.d.ts +5 -0
  39. package/dist/offline/crypto.js +24 -0
  40. package/dist/offline/index.d.ts +8 -0
  41. package/dist/offline/index.js +7 -0
  42. package/dist/offline/indexed-db.d.ts +10 -0
  43. package/dist/offline/indexed-db.js +104 -0
  44. package/dist/offline/management-types.d.ts +25 -0
  45. package/dist/offline/management-types.js +1 -0
  46. package/dist/offline/outbox.d.ts +7 -0
  47. package/dist/offline/outbox.js +46 -0
  48. package/dist/offline/replay.d.ts +25 -0
  49. package/dist/offline/replay.js +192 -0
  50. package/dist/offline/secure-store.d.ts +6 -0
  51. package/dist/offline/secure-store.js +51 -0
  52. package/dist/offline/types.d.ts +162 -0
  53. package/dist/offline/types.js +1 -0
  54. package/dist/orm-json-scalars.d.ts +21 -0
  55. package/dist/orm-json-scalars.js +21 -0
  56. package/dist/runtime-contracts.d.ts +7 -7
  57. package/dist/runtime-contracts.js +1 -1
  58. package/dist/sdk-orm.d.ts +7 -1
  59. package/dist/sdk-orm.js +12 -5
  60. package/dist/sdk.d.ts +134 -1
  61. package/dist/server/api-client.d.ts +26 -0
  62. package/dist/server/api-client.js +38 -0
  63. package/dist/server/broker-contract.d.ts +191 -5
  64. package/dist/server/broker-contract.js +142 -1
  65. package/dist/server/client.d.ts +82 -47
  66. package/dist/server/client.js +138 -59
  67. package/dist/server/connection-retry.d.ts +12 -0
  68. package/dist/server/connection-retry.js +40 -0
  69. package/dist/server/errors.d.ts +61 -0
  70. package/dist/server/errors.js +89 -0
  71. package/dist/server/external-context.d.ts +21 -0
  72. package/dist/server/external-context.js +19 -0
  73. package/dist/server/internal/signed-token.d.ts +15 -0
  74. package/dist/server/internal/signed-token.js +67 -0
  75. package/dist/server/internal/v1-context.d.ts +18 -0
  76. package/dist/server/internal/v1-context.js +26 -0
  77. package/dist/server/internal/verify-any.d.ts +22 -0
  78. package/dist/server/internal/verify-any.js +53 -0
  79. package/dist/server/logger.d.ts +1 -1
  80. package/dist/server/managed-server-path.d.ts +6 -0
  81. package/dist/server/managed-server-path.js +14 -0
  82. package/dist/server/principal.d.ts +48 -0
  83. package/dist/server/principal.js +39 -0
  84. package/dist/server/processes.d.ts +290 -0
  85. package/dist/server/processes.js +56 -0
  86. package/dist/server/request-context.d.ts +121 -3
  87. package/dist/server/request-context.js +111 -33
  88. package/dist/server/testing.d.ts +40 -0
  89. package/dist/server/testing.js +86 -0
  90. package/dist/server.d.ts +13 -4
  91. package/dist/server.js +6 -2
  92. package/dist/source-resolver.d.ts +9 -2
  93. package/dist/source-resolver.js +55 -8
  94. package/dist/templates/nextjs/AGENTS.md +10 -0
  95. package/dist/templates/nextjs/CLAUDE.md +1 -0
  96. package/dist/templates/nextjs/README.md +88 -0
  97. package/dist/templates/nextjs/package.json +3 -1
  98. package/dist/templates/nextjs/tests/server.test.mjs +25 -0
  99. package/dist/templates/react-vite/AGENTS.md +8 -88
  100. package/dist/templates/react-vite/CLAUDE.md +0 -6
  101. package/dist/templates/react-vite/package.json +1 -1
  102. package/dist/templates/react-vite/src/App.tsx +37 -14
  103. package/dist/templates/react-vite/src/styles.css +10 -0
  104. package/package.json +15 -3
  105. package/templates/nextjs/AGENTS.md +10 -0
  106. package/templates/nextjs/CLAUDE.md +1 -0
  107. package/templates/nextjs/README.md +88 -0
  108. package/templates/nextjs/package.json +3 -1
  109. package/templates/nextjs/tests/server.test.mjs +25 -0
  110. package/templates/react-vite/AGENTS.md +8 -88
  111. package/templates/react-vite/CLAUDE.md +0 -6
  112. package/templates/react-vite/package.json +1 -1
  113. package/templates/react-vite/src/App.tsx +37 -14
  114. package/templates/react-vite/src/styles.css +10 -0
package/README.md CHANGED
@@ -36,15 +36,49 @@ contract for external clients.
36
36
  The adapter validates the namespace and manifest, rejects unknown operations,
37
37
  and returns the operation's `data`. Errors retain `OrmOperationError.status`,
38
38
  `code`, and `errors`; missing host context or malformed success responses fail
39
- instead of reporting saved data. Calls are never automatically retried. For
39
+ instead of reporting saved data. The adapter never retries a call automatically. For
40
40
  several operations in one traced UI action, pass `withTraceAction`'s client as
41
41
  the optional `sdk` argument.
42
42
 
43
+ ### Exact numbers
44
+
45
+ By default `numeric` and `bigint` values come back as JSON numbers, which
46
+ JavaScript rounds past 15-17 significant digits; a form that saves what it read
47
+ would write the rounded value back. Ask for strings instead:
48
+
49
+ ```ts
50
+ const orm = createArtifactOrmExecutor({ namespace: 'lending', manifest, jsonScalars: 'lossless-v1' })
51
+ // A Next.js server component, route handler or Server Action:
52
+ const db = client.orm({ namespace: 'lending', manifest, jsonScalars: 'lossless-v1' })
53
+ ```
54
+
55
+ Results then carry these columns as strings (`"250000.00"`,
56
+ `"9007199254740993"`). The executor refuses any successful result the platform
57
+ does not acknowledge in that form, with `OrmOperationError` code
58
+ `LOSSLESS_TRANSPORT_UNAVAILABLE`, so an older Atlas can never hand back rounded
59
+ money as exact. Send `numeric` and `bigint` variables as decimal strings in
60
+ either mode, and avoid `Number`/`parseFloat` on exact values.
61
+
43
62
  Artifact Studio is a workflow-agnostic platform primitive. The host, SDK, and
44
63
  deployer provide generic application capabilities; each deployed artifact owns
45
64
  its product behavior. Primitive code must not branch on an artifact ID or import
46
65
  an application's services. See [Platform primitive boundaries](../../../docs/platform/platform-primitives.md).
47
66
 
67
+ ## Lost connections in a Next.js server
68
+
69
+ The server client (`@sequenceholdings/artifact-studio/server`) sends a read up
70
+ to three times when its connection to Atlas fails before Atlas answers: the
71
+ connection was refused or reset, the host or network was unreachable, or a DNS
72
+ lookup failed transiently. It pauses up to 100 ms, then up to 200 ms, and logs a
73
+ `broker.reconnect` warning for each resend. Reads are `client.orm` operations
74
+ the manifest lists with `kind: 'query'`, `client.api` GET and HEAD requests,
75
+ Storage GET and HEAD requests, and flag evaluations. ORM mutations, other API
76
+ methods, Storage writes, function invokes and process starts are sent once,
77
+ because a reset can come after Atlas acted on them, and reject with fetch's
78
+ `TypeError: fetch failed` as before. (Audit delivery keeps its own single
79
+ resend under the same event id.) A response with any status is Atlas's answer
80
+ and is never resent.
81
+
48
82
  ## Grouping calls into a trace
49
83
 
50
84
  The Artifact host supplies tracing context automatically for supported platform
package/VERSION-PIN.md CHANGED
@@ -21,6 +21,35 @@ Artifact Studio 0.3.1 (Studio CLI 0.2.2) verifies the per-request `jti` that the
21
21
  gateway now signs into every request context. The 0.3.0 context schema predates
22
22
  that claim and rejects those contexts, so Next.js applications need 0.3.1 or newer.
23
23
 
24
+ Artifact Studio 0.6.0 (Studio CLI 0.4.2) adds external request contexts (v2)
25
+ for callers that are not Atlas users, admitted through a manifest-declared
26
+ ingress. 0.5.0 was never published; 0.6.0 also ships its server `flags`,
27
+ `audit` and `jsonScalars` surface and the Lattice `processes.read` reads
28
+ (`listRuns`, `getRun`, `describe`, `listTasks`) and `processes.act` writes
29
+ (`advance`, `assign`, run viewers, `searchPrincipals`). The public verifiers
30
+ (`verifyArtifactRequestContext`, `getArtifactRequestContext`) stay v1-only and
31
+ refuse a v2 context, or any token signed with an `external-` kid, with reason
32
+ `external_context_not_accepted`. A handler accepts external callers only
33
+ through `verifyArtifactExternalContext` or `createArtifactServerClient({ accept })`,
34
+ and an external principal reaches platform services only through `asService()`.
35
+ `@sequenceholdings/artifact-studio/server/testing` signs test contexts.
36
+ 0.6.0 also carries `error.atlas` (`{ code, detail }`) on a refused broker call
37
+ (#13398); a step-up 401 still surfaces as `insufficient_user_authentication`.
38
+
39
+ Context version policy: a context verifier rejects any unknown top-level or
40
+ `principal` key unless it sits under `ext` or is named in `crit` (and `crit`
41
+ names only extensions the verifier understands; today none, so it is empty).
42
+ Security-relevant additions bump `v`; they are never added as optional keys.
43
+
44
+ The Artifact mobile-pattern source adds new atlas-ui exports. Roll that surface
45
+ out in dependency order: publish atlas-ui from its changeset, update the exact
46
+ `@sequenceholdings/atlas-ui` dependency in `artifact-studio/package.json`,
47
+ publish Artifact Studio and Studio CLI, then migrate and redeploy artifact
48
+ consumers. A source-linked candidate preview may exercise the new exports
49
+ before publication; an ordinary released CLI intentionally cannot.
50
+ The React/Vite starter therefore uses released primitives and responsive CSS
51
+ until that pin advances. Its build test exercises the ordinary CLI toolchain.
52
+
24
53
  ## What this means
25
54
 
26
55
  `shared/services/artifact-studio/src/build.ts` force-aliases platform peers via
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Official (active-channel) Artifact Studio deploys must come from `main` with
3
- * a resolved commit and a clean artifact source directory — whether the source
4
- * is a platform git-service repo (`--repo`) or a monorepo / local checkout.
2
+ * Official (active-channel) Artifact Studio deploys must come from `main`, or
3
+ * from `staging` when the target is a staging environment, with a resolved
4
+ * commit and a clean artifact source directory — whether the source is a
5
+ * platform git-service repo (`--repo`) or a monorepo / local checkout.
5
6
  * Preview deploys may use feature branches, dirty trees, or other refs.
6
7
  *
7
8
  * This checks reported provenance (branch name + explicit clean flag + commit).
@@ -19,9 +20,12 @@ export interface ActiveDeployProvenance {
19
20
  gitCommit?: string | null;
20
21
  gitBranch?: string | null;
21
22
  gitDirty?: boolean | null;
23
+ environment?: string | null;
22
24
  }
23
25
  /** Canonical default branch name for official artifact deploys. */
24
26
  export declare const ACTIVE_DEPLOY_DEFAULT_BRANCH = "main";
27
+ /** Branch allowed for official artifact deploys to staging environments. */
28
+ export declare const ACTIVE_DEPLOY_STAGING_BRANCH = "staging";
25
29
  export declare class ActiveDeployPolicyError extends Error {
26
30
  readonly code: "ACTIVE_DEPLOY_POLICY";
27
31
  constructor(message: string);
@@ -29,6 +33,8 @@ export declare class ActiveDeployPolicyError extends Error {
29
33
  /** Strip `refs/heads/` so `refs/heads/main` and `main` compare equal. */
30
34
  export declare function normalizeGitBranchName(branch: string | null | undefined): string | null;
31
35
  export declare function isActiveDeployDefaultBranch(branch: string | null | undefined): boolean;
36
+ /** Recognize first-party and provisioned staging environment names. */
37
+ export declare function isStagingEnvironment(environment: string | null | undefined): boolean;
32
38
  /**
33
39
  * Throw if provenance is not eligible for an official (active) deploy.
34
40
  * Preview callers must not invoke this.
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Official (active-channel) Artifact Studio deploys must come from `main` with
3
- * a resolved commit and a clean artifact source directory — whether the source
4
- * is a platform git-service repo (`--repo`) or a monorepo / local checkout.
2
+ * Official (active-channel) Artifact Studio deploys must come from `main`, or
3
+ * from `staging` when the target is a staging environment, with a resolved
4
+ * commit and a clean artifact source directory — whether the source is a
5
+ * platform git-service repo (`--repo`) or a monorepo / local checkout.
5
6
  * Preview deploys may use feature branches, dirty trees, or other refs.
6
7
  *
7
8
  * This checks reported provenance (branch name + explicit clean flag + commit).
@@ -17,6 +18,8 @@
17
18
  */
18
19
  /** Canonical default branch name for official artifact deploys. */
19
20
  export const ACTIVE_DEPLOY_DEFAULT_BRANCH = 'main';
21
+ /** Branch allowed for official artifact deploys to staging environments. */
22
+ export const ACTIVE_DEPLOY_STAGING_BRANCH = 'staging';
20
23
  export class ActiveDeployPolicyError extends Error {
21
24
  code = 'ACTIVE_DEPLOY_POLICY';
22
25
  constructor(message) {
@@ -36,6 +39,11 @@ export function normalizeGitBranchName(branch) {
36
39
  export function isActiveDeployDefaultBranch(branch) {
37
40
  return normalizeGitBranchName(branch) === ACTIVE_DEPLOY_DEFAULT_BRANCH;
38
41
  }
42
+ /** Recognize first-party and provisioned staging environment names. */
43
+ export function isStagingEnvironment(environment) {
44
+ const normalized = environment?.trim().toLowerCase();
45
+ return normalized === 'staging' || normalized?.endsWith('_staging') === true || normalized?.endsWith('-staging') === true;
46
+ }
39
47
  /**
40
48
  * Throw if provenance is not eligible for an official (active) deploy.
41
49
  * Preview callers must not invoke this.
@@ -52,18 +60,22 @@ export function assertActiveDeployProvenance(provenance) {
52
60
  }
53
61
  const branch = normalizeGitBranchName(provenance.gitBranch);
54
62
  if (!branch) {
55
- throw new ActiveDeployPolicyError('Active (official) artifact deploys require git provenance on `main`. ' +
56
- 'Check out `main` (monorepo or local clone) or deploy with ' +
63
+ throw new ActiveDeployPolicyError('Active (official) artifact deploys require git provenance on `main` ' +
64
+ 'or `staging` for a staging target. ' +
65
+ 'Check out `main` (or `staging` for staging) in a monorepo or local clone, or deploy with ' +
57
66
  '`seq-studio artifact deploy --repo <ns>/<slug>` (defaults to the repo default branch). ' +
58
67
  'Feature branches and detached HEAD (unless at the tip of `main`) are preview-only.');
59
68
  }
60
- if (branch !== ACTIVE_DEPLOY_DEFAULT_BRANCH) {
69
+ const stagingBranchAllowed = branch === ACTIVE_DEPLOY_STAGING_BRANCH && isStagingEnvironment(provenance.environment);
70
+ if (branch !== ACTIVE_DEPLOY_DEFAULT_BRANCH && !stagingBranchAllowed) {
61
71
  throw new ActiveDeployPolicyError(`Active (official) artifact deploys must use \`${ACTIVE_DEPLOY_DEFAULT_BRANCH}\` ` +
72
+ `or \`${ACTIVE_DEPLOY_STAGING_BRANCH}\` for staging targets ` +
62
73
  `(got branch/ref \`${branch}\`). Merge via a Pull, then redeploy from \`main\`; ` +
63
74
  'use a preview channel for feature-branch or dirty local work.');
64
75
  }
65
76
  if (!provenance.gitCommit?.trim()) {
66
- throw new ActiveDeployPolicyError('Active (official) artifact deploys require a resolved git commit on `main`. ' +
67
- 'Deploy from a git checkout or `--repo` so provenance is recorded.');
77
+ throw new ActiveDeployPolicyError('Active (official) artifact deploys require a resolved git commit on `main` ' +
78
+ 'or `staging` for a staging target. Deploy from a git checkout or `--repo` ' +
79
+ 'so provenance is recorded.');
68
80
  }
69
81
  }
@@ -0,0 +1,9 @@
1
+ /** Fail an artifact deploy whose source still points at another environment. */
2
+ export declare function assertArtifactEnvironmentBindings({ directory, environment, token, }: {
3
+ directory: string;
4
+ environment: {
5
+ name: string;
6
+ url: string;
7
+ };
8
+ token: string;
9
+ }): Promise<void>;
@@ -0,0 +1,32 @@
1
+ import { getJsonOr404 } from './api.js';
2
+ import { knownEnvUrls } from './config.js';
3
+ import { assertNoCrossEnvironmentBindings, canonicalDeploymentOrigin } from './env-bindings.js';
4
+ import { readArtifactStudioSource } from './project.js';
5
+ /** Fail an artifact deploy whose source still points at another environment. */
6
+ export async function assertArtifactEnvironmentBindings({ directory, environment, token, }) {
7
+ const source = await readArtifactStudioSource(directory);
8
+ const deployOrigin = canonicalDeploymentOrigin(environment.url);
9
+ if (!deployOrigin)
10
+ throw new Error(`Invalid deployment URL '${environment.url}'.`);
11
+ const origins = Object.entries(knownEnvUrls()).flatMap(([name, url]) => {
12
+ const origin = canonicalDeploymentOrigin(url);
13
+ return origin ? [{ name, origin }] : [];
14
+ });
15
+ await assertNoCrossEnvironmentBindings({
16
+ deployEnvironment: environment.name,
17
+ deployOrigin,
18
+ origins,
19
+ workspaces: [],
20
+ files: source.files,
21
+ workspaceExists: async (workspaceId) => {
22
+ const workspace = await getJsonOr404({
23
+ baseUrl: environment.url,
24
+ token,
25
+ path: `/api/storage/workspaces/${workspaceId}`,
26
+ });
27
+ // The selected API already scopes this read to its storage environment,
28
+ // including local `local:<project>:<branch>` values.
29
+ return workspace !== null;
30
+ },
31
+ });
32
+ }
package/dist/build.js CHANGED
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir, writeFile } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
4
  import { dirname, relative, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import { build as viteBuild } from 'vite';
6
7
  import { atlasUiCssResolverPlugin, collectAtlasUiResolvedTokenSelections, hasUnifiedAtlasUiTokens } from './atlas-ui-css-plugin.js';
7
8
  import { validateAtlasUiTokenStylesheetContract } from './atlas-ui-package-contract.js';
@@ -51,6 +52,10 @@ const atlasUiTokens = resolveAtlasUiTokensPath(atlasUiDir);
51
52
  // `seq-studio dev --link`) work.
52
53
  const formRendererDir = dirname(cliRequire.resolve('@sequenceholdings/lattice-form-renderer/package.json'));
53
54
  const formRendererDist = resolve(formRendererDir, 'dist');
55
+ const artifactStudioModuleDir = dirname(fileURLToPath(import.meta.url));
56
+ const offlineRuntimeEntry = existsSync(resolve(artifactStudioModuleDir, 'offline/index.js'))
57
+ ? resolve(artifactStudioModuleDir, 'offline/index.js')
58
+ : resolve(artifactStudioModuleDir, 'offline/index.ts');
54
59
  /**
55
60
  * Import aliases for the Vite build. Artifacts are built against their own
56
61
  * source tree, which usually has no installed node_modules, so every bare
@@ -79,6 +84,7 @@ function createPeerResolveAlias({ atlasUiPackageDir = atlasUiDir } = {}) {
79
84
  sonner: sonnerDir,
80
85
  '@sequenceholdings/storage': cliRequire.resolve('@sequenceholdings/storage'),
81
86
  '@sequenceholdings/orm/client': cliRequire.resolve('@sequenceholdings/orm/client'),
87
+ '@sequenceholdings/artifact-studio/offline': offlineRuntimeEntry,
82
88
  // Candidate Atlas UI exposes canonical tokens at the package root; released
83
89
  // 2.0.1 keeps them in dist. Preserve explicit alias resolution so legacy
84
90
  // source reaches the normalizer instead of falling through to the bare alias.
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@ 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 { assertArtifactEnvironmentBindings } from './artifact-env-bindings.js';
19
20
  import { normalizeArtifactPath } from './paths.js';
20
21
  import yaml from 'js-yaml';
21
22
  import { warnOnRoleDefinitions } from './role-definitions-notice.js';
@@ -29,23 +30,26 @@ const USAGE = `usage:
29
30
  seq-studio artifact link --env <env> [--project <id>]
30
31
  seq-studio artifact env list|use <env>
31
32
  seq-studio artifact status [--env <env>]
32
- seq-studio artifact list --env <env>
33
- seq-studio artifact show <slug-or-id> --env <env>
33
+ seq-studio artifact list --env <env> [--json]
34
+ seq-studio artifact show <slug-or-id> --env <env> [--json]
34
35
  seq-studio artifact validate [dir]
35
- seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--out dist/artifact-bundle.json]
36
- seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] --env <env>
37
- 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 build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--out dist/artifact-bundle.json]
37
+ seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] --env <env>
38
+ seq-studio artifact deploy [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] --env <env> [--skip-unchanged] [--no-create] [--project <id>]
38
39
  seq-studio artifact dev [dir] --env <env> [--once] [--key default] [--link <pkgDir>[,<pkgDir>]] [--port <port>]
39
40
  seq-studio artifact promote <deployment-id> --env <env>
40
41
  seq-studio artifact pull <project-id> --env <env> --out <dir>
41
42
  seq-studio artifact rollback <deployment-id> --env <env>
42
43
 
43
44
  Add --help or -h to any artifact command to print this usage without running it.
45
+ init includes AGENTS.md pointing to your environment's live docs, with a
46
+ CLAUDE.md import, for both templates.
44
47
 
45
48
  Source: a local [dir] (default), a platform git-service repo (--repo <ns>/<name>),
46
49
  or a public HTTPS git URL (--git-url <url>). --ref picks a branch/tag/commit (default: the
47
- repo's default branch). The target project comes from the source's
48
- artifact.bundle.yml project_id.
50
+ repo's default branch). --path <dir> selects the application directory inside a
51
+ --repo / --git-url repository (default: its root). The target project comes from
52
+ the source's artifact.bundle.yml project_id.
49
53
 
50
54
  Official (active) deploys to deployed environments require \`main\` with a
51
55
  resolved git commit and a clean artifact source directory. Dirty files
@@ -83,6 +87,11 @@ export async function runCli(argv = process.argv.slice(2)) {
83
87
  console.error(parsed.error);
84
88
  return 1;
85
89
  }
90
+ const optionError = validateKnownOptions(parsed);
91
+ if (optionError) {
92
+ console.error(optionError);
93
+ return 1;
94
+ }
86
95
  switch (parsed.command) {
87
96
  case 'init': return initCommand(parsed);
88
97
  case 'whoami': return whoamiCommand(parsed);
@@ -101,6 +110,34 @@ export async function runCli(argv = process.argv.slice(2)) {
101
110
  case 'rollback': return rollbackCommand(parsed);
102
111
  }
103
112
  }
113
+ function validateKnownOptions(args) {
114
+ const options = {
115
+ init: { values: ['template'], booleans: [] }, whoami: { values: ['env'], booleans: [] },
116
+ link: { values: ['env', 'project'], booleans: [] }, env: { values: [], booleans: [] },
117
+ status: { values: ['env'], booleans: [] }, list: { values: ['env'], booleans: ['json'] },
118
+ show: { values: ['env'], booleans: ['json'] }, validate: { values: [], booleans: [] },
119
+ build: { values: ['env', 'repo', 'git-url', 'ref', 'path', 'out'], booleans: [] },
120
+ plan: { values: ['env', 'repo', 'git-url', 'ref', 'path'], booleans: [] },
121
+ deploy: { values: ['env', 'repo', 'git-url', 'ref', 'path', 'project', 'out'], booleans: ['skip-unchanged', 'no-create'] },
122
+ dev: { values: ['env', 'key', 'link', 'port'], booleans: ['once'] },
123
+ promote: { values: ['env'], booleans: [] }, pull: { values: ['env', 'out'], booleans: [] },
124
+ rollback: { values: ['env'], booleans: [] },
125
+ };
126
+ const { values, booleans } = options[args.command];
127
+ const allowed = new Set([...values, ...booleans]);
128
+ for (const key of Object.keys(args.flags)) {
129
+ if (!allowed.has(key)) {
130
+ return `Unknown option --${key} for artifact ${args.command}. Run seq-studio artifact --help.`;
131
+ }
132
+ if (values.includes(key) && (args.flags[key] === true || args.flags[key]?.trim() === '')) {
133
+ return `Option --${key} requires a value for artifact ${args.command}.`;
134
+ }
135
+ if (booleans.includes(key) && args.flags[key] !== true) {
136
+ return `Option --${key} does not take a value for artifact ${args.command}.`;
137
+ }
138
+ }
139
+ return undefined;
140
+ }
104
141
  function parseArgs(argv) {
105
142
  if (argv[0] === '--')
106
143
  argv = argv.slice(1);
@@ -158,8 +195,9 @@ async function initCommand(args) {
158
195
  }
159
196
  }
160
197
  console.log(`[seq-studio] initialized ${dir}`);
198
+ console.log('AGENTS.md points to your environment\'s live docs; CLAUDE.md imports it.');
161
199
  console.log(template === 'nextjs'
162
- ? '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.'
200
+ ? 'Next: use Node.js 24, cd into the app and run pnpm install, then pnpm run lint && pnpm run type-check && pnpm test. Commit pnpm-lock.yaml before deploying. seq-studio login && seq-studio artifact dev --env <env> starts development; repository CI and hosted builds use Buildkite. No cloud credentials are required.'
163
201
  : 'Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
164
202
  return 0;
165
203
  }
@@ -273,6 +311,10 @@ async function listCommand(args) {
273
311
  token,
274
312
  path: '/api/artifact-studio/projects',
275
313
  });
314
+ if (args.flags.json === true) {
315
+ console.log(JSON.stringify(data, null, 2));
316
+ return 0;
317
+ }
276
318
  if (data.projects.length === 0) {
277
319
  console.log(`[seq-studio] no artifact projects visible on ${env.name}`);
278
320
  return 0;
@@ -326,6 +368,10 @@ async function showCommand(args) {
326
368
  console.error(`[seq-studio] project ${projectId} not found on ${env.name}`);
327
369
  return 1;
328
370
  }
371
+ if (args.flags.json === true) {
372
+ console.log(JSON.stringify(data, null, 2));
373
+ return 0;
374
+ }
329
375
  const project = data.project;
330
376
  console.log(`[seq-studio] project on ${env.name}:`);
331
377
  console.log(` id: ${project.id}`);
@@ -504,11 +550,13 @@ async function deployCommand(args) {
504
550
  // Fail closed before build / project create for deployed environments.
505
551
  // Local Atlas is a developer's own target, so it intentionally accepts
506
552
  // the current branch and dirty working tree.
507
- if (env.name !== 'local')
508
- assertActiveDeployProvenance(source.provenance);
553
+ if (env.name !== 'local') {
554
+ assertActiveDeployProvenance({ ...source.provenance, environment: env.name });
555
+ }
509
556
  const manifest = await readArtifactStudioManifest(source.dir);
510
557
  // After source resolution, so --repo / --git-url deploys check the fetched manifest.
511
558
  warnOnRoleDefinitions(manifest);
559
+ await assertArtifactEnvironmentBindings({ directory: source.dir, environment: env, token });
512
560
  if (isNextArtifactManifest(manifest)) {
513
561
  return await buildNextCommand({ args, source, active: true });
514
562
  }
@@ -569,8 +617,9 @@ async function buildNextCommand({ args, source, active }) {
569
617
  const { nextSourceRepository, resolveNextRepositoryId, submitNextBuild } = await import('./next-build.js');
570
618
  const spec = parseSourceSpec(args);
571
619
  const repository = nextSourceRepository({ spec, root: source.dir });
572
- if (active)
573
- assertActiveDeployProvenance(source.provenance);
620
+ if (active) {
621
+ assertActiveDeployProvenance({ ...source.provenance, environment: env.name });
622
+ }
574
623
  const repositoryId = active || spec.kind === 'git-service'
575
624
  ? await resolveNextRepositoryId({ spec, repository, baseUrl: env.url, token })
576
625
  : undefined;
@@ -583,7 +632,7 @@ async function buildNextCommand({ args, source, active }) {
583
632
  console.log('[seq-studio] Next.js submits source to Atlas for build verification; local static-bundle skip checks do not apply.');
584
633
  }
585
634
  return submitNextBuild({
586
- root: source.dir, manifest, projectId: project.id, baseUrl: env.url,
635
+ root: source.dir, sourceRoot: source.sourceRoot, manifest, projectId: project.id, baseUrl: env.url,
587
636
  getToken: () => getRequiredToken(args), provenance: source.provenance, repository, repositoryId,
588
637
  channel: active ? 'active' : 'preview', out: typeof args.flags.out === 'string' ? resolve(args.flags.out) : undefined,
589
638
  });
@@ -7,3 +7,5 @@ export declare function authenticatedRequestUrl({ baseUrl, path, }: {
7
7
  baseUrl: string;
8
8
  path: string;
9
9
  }): string;
10
+ export { assertNoCrossEnvironmentBindings, canonicalDeploymentOrigin, findCrossEnvironmentBindings, type BindingSourceFile, type CrossEnvironmentBinding, type NamedOrigin, type NamedWorkspace, } from './env-bindings.js';
11
+ export { readArtifactStudioSource } from './project.js';
@@ -68,3 +68,5 @@ export function authenticatedRequestUrl({ baseUrl, path, }) {
68
68
  }
69
69
  return request.href;
70
70
  }
71
+ export { assertNoCrossEnvironmentBindings, canonicalDeploymentOrigin, findCrossEnvironmentBindings, } from './env-bindings.js';
72
+ export { readArtifactStudioSource } from './project.js';
package/dist/dev-link.js CHANGED
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
4
4
  import { cp, mkdir, readFile, readdir, rm } from 'node:fs/promises';
5
5
  import { createRequire } from 'node:module';
6
- import { dirname, join, relative, resolve } from 'node:path';
6
+ import { basename, dirname, join, relative, resolve } from 'node:path';
7
7
  /** True only when this dev session explicitly selected a local Atlas UI package. */
8
8
  export function hasLinkedAtlasUiToolchain({ deps }) {
9
9
  return deps.some((dep) => dep.name === '@sequenceholdings/atlas-ui');
@@ -168,6 +168,16 @@ export function rebuildLinkedDep(dep, signal, env) {
168
168
  });
169
169
  });
170
170
  }
171
+ function exportsReferencePath(value, path) {
172
+ if (typeof value === 'string') {
173
+ const pattern = value.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*');
174
+ return new RegExp('^' + pattern + '$').test(path);
175
+ }
176
+ if (value && typeof value === 'object') {
177
+ return Object.values(value).some((entry) => exportsReferencePath(entry, path));
178
+ }
179
+ return false;
180
+ }
171
181
  /**
172
182
  * Copy a linked package's freshly built `dist` and root CSS entries into the
173
183
  * location the artifact build resolves them from. A no-op when the artifact
@@ -181,10 +191,26 @@ export async function syncLinkedDist(dep) {
181
191
  throw new Error(`${dep.name} build produced no dist at ${dep.distDir}`);
182
192
  }
183
193
  const targetPkgDir = dirname(dep.targetDistDir);
194
+ const targetManifest = join(targetPkgDir, 'package.json');
195
+ const targetExports = existsSync(targetManifest)
196
+ ? JSON.parse(await readFile(targetManifest, 'utf8')).exports
197
+ : undefined;
184
198
  await mkdir(targetPkgDir, { recursive: true });
185
199
  await rm(dep.targetDistDir, { recursive: true, force: true });
186
200
  await cp(dep.distDir, dep.targetDistDir, { recursive: true });
187
201
  for (const file of await packageRootCssFiles(dep.dir)) {
188
- await cp(file, join(targetPkgDir, relative(dep.dir, file)));
202
+ // Published packages can export a compiled sheet from dist while the
203
+ // linked source package also keeps its generated source sheet at root.
204
+ // Copying both creates two candidates for the same subpath and violates
205
+ // the package contract. Only remove it when the installed exports map
206
+ // explicitly owns the dist path and no condition still needs the root.
207
+ const targetRootFile = join(targetPkgDir, relative(dep.dir, file));
208
+ if (existsSync(join(dep.distDir, basename(file))) &&
209
+ exportsReferencePath(targetExports, './dist/' + basename(file)) &&
210
+ !exportsReferencePath(targetExports, './' + basename(file))) {
211
+ await rm(targetRootFile, { force: true });
212
+ continue;
213
+ }
214
+ await cp(file, targetRootFile);
189
215
  }
190
216
  }
@@ -0,0 +1,10 @@
1
+ import type { BindingSourceFile, NamedOrigin } from './env-bindings.js';
2
+ export declare function originsDeclaredInFiles(files: readonly BindingSourceFile[]): NamedOrigin[];
3
+ export declare function declaredTargets(content: string): Record<string, string>;
4
+ export declare function contentContainsOrigin(content: string, origin: string, skippedLines: ReadonlySet<number>): boolean;
5
+ /** Manifest text with the root `targets` map removed, so its URLs are declarations. */
6
+ export declare function contentOutsideTargets(content: string): string;
7
+ export declare function collectWorkspaceSlotIds(files: readonly BindingSourceFile[]): string[];
8
+ export declare function workspaceSlotIds(file: BindingSourceFile): string[];
9
+ export declare function scannable(file: BindingSourceFile): boolean;
10
+ export declare function basename(path: string): string;