@sequenceholdings/artifact-studio 0.3.1 → 0.5.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 (108) hide show
  1. package/README.md +71 -0
  2. package/VERSION-PIN.md +9 -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 +7 -0
  8. package/dist/cli.js +66 -13
  9. package/dist/deployment-validation.d.ts +2 -0
  10. package/dist/deployment-validation.js +2 -0
  11. package/dist/dev-link.js +29 -3
  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.d.ts +3 -0
  25. package/dist/gateway/server.js +4 -5
  26. package/dist/gateway/session.d.ts +17 -2
  27. package/dist/gateway/session.js +27 -6
  28. package/dist/gateway-main.js +2 -0
  29. package/dist/loopback.d.ts +8 -0
  30. package/dist/loopback.js +13 -0
  31. package/dist/manifest-custom-roles.d.ts +11 -1
  32. package/dist/manifest-custom-roles.js +13 -5
  33. package/dist/manifest-next.d.ts +55 -0
  34. package/dist/manifest-next.js +35 -2
  35. package/dist/manifest-static.d.ts +35 -0
  36. package/dist/manifest-static.js +12 -0
  37. package/dist/manifest.d.ts +48 -0
  38. package/dist/next/protocol.js +2 -1
  39. package/dist/next-build.d.ts +2 -1
  40. package/dist/next-build.js +12 -7
  41. package/dist/next-dev.js +2 -1
  42. package/dist/next-project.js +2 -1
  43. package/dist/offline/cache.d.ts +4 -0
  44. package/dist/offline/cache.js +7 -0
  45. package/dist/offline/connector.d.ts +26 -0
  46. package/dist/offline/connector.js +118 -0
  47. package/dist/offline/crypto.d.ts +5 -0
  48. package/dist/offline/crypto.js +24 -0
  49. package/dist/offline/index.d.ts +8 -0
  50. package/dist/offline/index.js +7 -0
  51. package/dist/offline/indexed-db.d.ts +10 -0
  52. package/dist/offline/indexed-db.js +104 -0
  53. package/dist/offline/outbox.d.ts +7 -0
  54. package/dist/offline/outbox.js +46 -0
  55. package/dist/offline/replay.d.ts +25 -0
  56. package/dist/offline/replay.js +192 -0
  57. package/dist/offline/secure-store.d.ts +6 -0
  58. package/dist/offline/secure-store.js +51 -0
  59. package/dist/offline/types.d.ts +162 -0
  60. package/dist/offline/types.js +1 -0
  61. package/dist/orm-json-scalars.d.ts +21 -0
  62. package/dist/orm-json-scalars.js +21 -0
  63. package/dist/project.js +3 -3
  64. package/dist/role-definitions-notice.d.ts +7 -0
  65. package/dist/role-definitions-notice.js +19 -0
  66. package/dist/runtime-contracts.d.ts +7 -7
  67. package/dist/runtime-contracts.js +2 -1
  68. package/dist/sandbox-lint.js +1 -1
  69. package/dist/sdk-orm.d.ts +24 -0
  70. package/dist/sdk-orm.js +28 -0
  71. package/dist/sdk.d.ts +127 -0
  72. package/dist/sdk.js +1 -0
  73. package/dist/server/broker-contract.d.ts +191 -5
  74. package/dist/server/broker-contract.js +142 -1
  75. package/dist/server/client.d.ts +60 -12
  76. package/dist/server/client.js +143 -20
  77. package/dist/server/errors.d.ts +26 -0
  78. package/dist/server/errors.js +29 -0
  79. package/dist/server/managed-server-path.d.ts +6 -0
  80. package/dist/server/managed-server-path.js +14 -0
  81. package/dist/server/processes.d.ts +290 -0
  82. package/dist/server/processes.js +56 -0
  83. package/dist/server/request-context.d.ts +6 -0
  84. package/dist/server/request-context.js +12 -2
  85. package/dist/server/runtime-config.js +2 -1
  86. package/dist/server.d.ts +5 -1
  87. package/dist/server.js +1 -0
  88. package/dist/source-resolver.d.ts +9 -2
  89. package/dist/source-resolver.js +55 -8
  90. package/dist/templates/nextjs/AGENTS.md +10 -0
  91. package/dist/templates/nextjs/CLAUDE.md +1 -0
  92. package/dist/templates/nextjs/README.md +88 -0
  93. package/dist/templates/react-vite/AGENTS.md +8 -88
  94. package/dist/templates/react-vite/CLAUDE.md +0 -6
  95. package/dist/templates/react-vite/package.json +1 -1
  96. package/dist/templates/react-vite/src/App.tsx +37 -14
  97. package/dist/templates/react-vite/src/styles.css +10 -0
  98. package/dist/tunnel/client.js +10 -3
  99. package/dist/tunnel/protocol.js +2 -1
  100. package/package.json +7 -3
  101. package/templates/nextjs/AGENTS.md +10 -0
  102. package/templates/nextjs/CLAUDE.md +1 -0
  103. package/templates/nextjs/README.md +88 -0
  104. package/templates/react-vite/AGENTS.md +8 -88
  105. package/templates/react-vite/CLAUDE.md +0 -6
  106. package/templates/react-vite/package.json +1 -1
  107. package/templates/react-vite/src/App.tsx +37 -14
  108. package/templates/react-vite/src/styles.css +10 -0
package/README.md CHANGED
@@ -3,11 +3,82 @@
3
3
  `@sequenceholdings/artifact-studio` exposes the host-provided `seq` client. Runtime
4
4
  requests use the current user's normal permissions.
5
5
 
6
+ ## Governed ORM operations
7
+
8
+ Use the published caller-aware adapter inside an Artifact Studio Preview or a
9
+ deployed artifact. Generate and apply the namespace first, then import its
10
+ `operations.manifest.json` into your application:
11
+
12
+ ```ts
13
+ import { createArtifactOrmExecutor } from '@sequenceholdings/artifact-studio'
14
+ import manifest from './operations.manifest.json'
15
+
16
+ const orm = createArtifactOrmExecutor({ namespace: 'team_directory', manifest })
17
+ const result = await orm.execute('Profiles', { limit: 100, offset: 0 })
18
+ ```
19
+
20
+ Declare the exact namespace path in `artifact.bundle.yml`:
21
+
22
+ ```yaml
23
+ capabilities:
24
+ api:
25
+ write: ['/api/data/v2/team_directory']
26
+ ```
27
+
28
+ Persisted queries and mutations both use POST, so both require this `write`
29
+ declaration. This declaration permits the artifact to make the request; each
30
+ user still needs namespace runtime access and the operation's role/capability.
31
+ ORM row policies and write checks enforce that caller's access. The helper uses
32
+ host authentication and requires no token, custom header, or environment URL.
33
+ The namespace path above is a capability declaration, not a direct HTTP API
34
+ contract for external clients.
35
+
36
+ The adapter validates the namespace and manifest, rejects unknown operations,
37
+ and returns the operation's `data`. Errors retain `OrmOperationError.status`,
38
+ `code`, and `errors`; missing host context or malformed success responses fail
39
+ instead of reporting saved data. The adapter never retries a call automatically. For
40
+ several operations in one traced UI action, pass `withTraceAction`'s client as
41
+ the optional `sdk` argument.
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
+
6
62
  Artifact Studio is a workflow-agnostic platform primitive. The host, SDK, and
7
63
  deployer provide generic application capabilities; each deployed artifact owns
8
64
  its product behavior. Primitive code must not branch on an artifact ID or import
9
65
  an application's services. See [Platform primitive boundaries](../../../docs/platform/platform-primitives.md).
10
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
+
11
82
  ## Grouping calls into a trace
12
83
 
13
84
  The Artifact host supplies tracing context automatically for supported platform
package/VERSION-PIN.md CHANGED
@@ -21,6 +21,15 @@ 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
+ The Artifact mobile-pattern source adds new atlas-ui exports. Roll that surface
25
+ out in dependency order: publish atlas-ui from its changeset, update the exact
26
+ `@sequenceholdings/atlas-ui` dependency in `artifact-studio/package.json`,
27
+ publish Artifact Studio and Studio CLI, then migrate and redeploy artifact
28
+ consumers. A source-linked candidate preview may exercise the new exports
29
+ before publication; an ordinary released CLI intentionally cannot.
30
+ The React/Vite starter therefore uses released primitives and responsive CSS
31
+ until that pin advances. Its build test exercises the ordinary CLI toolchain.
32
+
24
33
  ## What this means
25
34
 
26
35
  `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
@@ -78,6 +83,8 @@ function createPeerResolveAlias({ atlasUiPackageDir = atlasUiDir } = {}) {
78
83
  'react-router-dom': reactRouterDomDir,
79
84
  sonner: sonnerDir,
80
85
  '@sequenceholdings/storage': cliRequire.resolve('@sequenceholdings/storage'),
86
+ '@sequenceholdings/orm/client': cliRequire.resolve('@sequenceholdings/orm/client'),
87
+ '@sequenceholdings/artifact-studio/offline': offlineRuntimeEntry,
81
88
  // Candidate Atlas UI exposes canonical tokens at the package root; released
82
89
  // 2.0.1 keeps them in dist. Preserve explicit alias resolution so legacy
83
90
  // source reaches the normalizer instead of falling through to the bare alias.
package/dist/cli.js CHANGED
@@ -16,8 +16,10 @@ 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';
22
+ import { warnOnRoleDefinitions } from './role-definitions-notice.js';
21
23
  const COMMANDS = [
22
24
  'init', 'whoami', 'link', 'env', 'status', 'list', 'show',
23
25
  'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
@@ -28,23 +30,26 @@ const USAGE = `usage:
28
30
  seq-studio artifact link --env <env> [--project <id>]
29
31
  seq-studio artifact env list|use <env>
30
32
  seq-studio artifact status [--env <env>]
31
- seq-studio artifact list --env <env>
32
- 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]
33
35
  seq-studio artifact validate [dir]
34
- seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--out dist/artifact-bundle.json]
35
- seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] --env <env>
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 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>]
37
39
  seq-studio artifact dev [dir] --env <env> [--once] [--key default] [--link <pkgDir>[,<pkgDir>]] [--port <port>]
38
40
  seq-studio artifact promote <deployment-id> --env <env>
39
41
  seq-studio artifact pull <project-id> --env <env> --out <dir>
40
42
  seq-studio artifact rollback <deployment-id> --env <env>
41
43
 
42
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.
43
47
 
44
48
  Source: a local [dir] (default), a platform git-service repo (--repo <ns>/<name>),
45
49
  or a public HTTPS git URL (--git-url <url>). --ref picks a branch/tag/commit (default: the
46
- repo's default branch). The target project comes from the source's
47
- 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.
48
53
 
49
54
  Official (active) deploys to deployed environments require \`main\` with a
50
55
  resolved git commit and a clean artifact source directory. Dirty files
@@ -82,6 +87,11 @@ export async function runCli(argv = process.argv.slice(2)) {
82
87
  console.error(parsed.error);
83
88
  return 1;
84
89
  }
90
+ const optionError = validateKnownOptions(parsed);
91
+ if (optionError) {
92
+ console.error(optionError);
93
+ return 1;
94
+ }
85
95
  switch (parsed.command) {
86
96
  case 'init': return initCommand(parsed);
87
97
  case 'whoami': return whoamiCommand(parsed);
@@ -100,6 +110,34 @@ export async function runCli(argv = process.argv.slice(2)) {
100
110
  case 'rollback': return rollbackCommand(parsed);
101
111
  }
102
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
+ }
103
141
  function parseArgs(argv) {
104
142
  if (argv[0] === '--')
105
143
  argv = argv.slice(1);
@@ -157,6 +195,7 @@ async function initCommand(args) {
157
195
  }
158
196
  }
159
197
  console.log(`[seq-studio] initialized ${dir}`);
198
+ console.log('AGENTS.md points to your environment\'s live docs; CLAUDE.md imports it.');
160
199
  console.log(template === 'nextjs'
161
200
  ? 'Next: cd into the app, run pnpm install, then seq-studio login && seq-studio artifact dev --env <env>. Commit pnpm-lock.yaml before deploying. No cloud credentials are required.'
162
201
  : 'Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
@@ -272,6 +311,10 @@ async function listCommand(args) {
272
311
  token,
273
312
  path: '/api/artifact-studio/projects',
274
313
  });
314
+ if (args.flags.json === true) {
315
+ console.log(JSON.stringify(data, null, 2));
316
+ return 0;
317
+ }
275
318
  if (data.projects.length === 0) {
276
319
  console.log(`[seq-studio] no artifact projects visible on ${env.name}`);
277
320
  return 0;
@@ -325,6 +368,10 @@ async function showCommand(args) {
325
368
  console.error(`[seq-studio] project ${projectId} not found on ${env.name}`);
326
369
  return 1;
327
370
  }
371
+ if (args.flags.json === true) {
372
+ console.log(JSON.stringify(data, null, 2));
373
+ return 0;
374
+ }
328
375
  const project = data.project;
329
376
  console.log(`[seq-studio] project on ${env.name}:`);
330
377
  console.log(` id: ${project.id}`);
@@ -503,9 +550,14 @@ async function deployCommand(args) {
503
550
  // Fail closed before build / project create for deployed environments.
504
551
  // Local Atlas is a developer's own target, so it intentionally accepts
505
552
  // the current branch and dirty working tree.
506
- if (env.name !== 'local')
507
- assertActiveDeployProvenance(source.provenance);
508
- if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
553
+ if (env.name !== 'local') {
554
+ assertActiveDeployProvenance({ ...source.provenance, environment: env.name });
555
+ }
556
+ const manifest = await readArtifactStudioManifest(source.dir);
557
+ // After source resolution, so --repo / --git-url deploys check the fetched manifest.
558
+ warnOnRoleDefinitions(manifest);
559
+ await assertArtifactEnvironmentBindings({ directory: source.dir, environment: env, token });
560
+ if (isNextArtifactManifest(manifest)) {
509
561
  return await buildNextCommand({ args, source, active: true });
510
562
  }
511
563
  // --skip-unchanged: hash the raw source (no build needed) and no-op when
@@ -565,8 +617,9 @@ async function buildNextCommand({ args, source, active }) {
565
617
  const { nextSourceRepository, resolveNextRepositoryId, submitNextBuild } = await import('./next-build.js');
566
618
  const spec = parseSourceSpec(args);
567
619
  const repository = nextSourceRepository({ spec, root: source.dir });
568
- if (active)
569
- assertActiveDeployProvenance(source.provenance);
620
+ if (active) {
621
+ assertActiveDeployProvenance({ ...source.provenance, environment: env.name });
622
+ }
570
623
  const repositoryId = active || spec.kind === 'git-service'
571
624
  ? await resolveNextRepositoryId({ spec, repository, baseUrl: env.url, token })
572
625
  : undefined;
@@ -579,7 +632,7 @@ async function buildNextCommand({ args, source, active }) {
579
632
  console.log('[seq-studio] Next.js submits source to Atlas for build verification; local static-bundle skip checks do not apply.');
580
633
  }
581
634
  return submitNextBuild({
582
- root: source.dir, manifest, projectId: project.id, baseUrl: env.url,
635
+ root: source.dir, sourceRoot: source.sourceRoot, manifest, projectId: project.id, baseUrl: env.url,
583
636
  getToken: () => getRequiredToken(args), provenance: source.provenance, repository, repositoryId,
584
637
  channel: active ? 'active' : 'preview', out: typeof args.flags.out === 'string' ? resolve(args.flags.out) : undefined,
585
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,12 +3,12 @@ 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');
10
10
  }
11
- const WATCHED_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.css', '.json']);
11
+ const WATCHED_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.css', '.json']);
12
12
  const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', '.next', '.turbo', '.cache']);
13
13
  const defaultResolver = createRequire(import.meta.url).resolve;
14
14
  /** Split a `--link a,b` flag value into trimmed, non-empty paths. */
@@ -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;