@sequenceholdings/artifact-studio 0.10.0 → 0.11.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.
package/README.md CHANGED
@@ -156,6 +156,41 @@ deployer provide generic application capabilities; each deployed artifact owns
156
156
  its product behavior. Primitive code must not branch on an artifact ID or import
157
157
  an application's services. See [Platform primitive boundaries](../../../docs/platform/platform-primitives.md).
158
158
 
159
+ ## Lattice processes from a static app (awaiting publication)
160
+
161
+ A static (React/Vite) artifact starts, reads and acts on Lattice runs with the
162
+ same operations a Next.js server uses. Declare each process id in
163
+ `artifact.bundle.yml`:
164
+
165
+ ```yaml
166
+ capabilities:
167
+ processes:
168
+ start: [maintenance-request] # start runs
169
+ read: [maintenance-request] # list and show runs, describe the process, the user's own tasks
170
+ act: [maintenance-request] # complete or reassign human steps, cancel, share runs
171
+ ```
172
+
173
+ ```ts
174
+ import { createArtifactProcesses } from '@sequenceholdings/artifact-studio'
175
+
176
+ const processes = createArtifactProcesses()
177
+ const run = await processes.start({ process: 'maintenance-request', metadata: { unit: '4B' } })
178
+ const snapshot = await processes.getRun(run.run_id)
179
+ const tasks = await processes.listTasks()
180
+ await processes.advance({ runId: run.run_id, payload: { approved: true } })
181
+ ```
182
+
183
+ Atlas checks every call against the deployed manifest's `capabilities.processes`,
184
+ not the browser's copy. A process it does not declare is refused with 403
185
+ before any grant is checked; declaring `/api/lattice/runs` under
186
+ `capabilities.api.write` is not a substitute, because that path is not scoped to
187
+ a process. The signed-in user's own grants then decide: Invoker to start,
188
+ Viewer to read, Advancer to complete a step, Owner to reassign or share.
189
+ Refusals raise `ArtifactServerError`, with Atlas's `detail` and `code` on
190
+ `error.atlas`. Group task pages, task assignments, batch describe and the
191
+ human-completion readers stay Next.js-only. This needs the matching Atlas
192
+ release; on an older host `createArtifactProcesses()` throws `processes_unavailable`.
193
+
159
194
  ## Lattice run lists and collaboration (internal prerelease)
160
195
 
161
196
  The server entry point remains an **internal** surface. These methods describe
@@ -20,6 +20,11 @@ export interface ActiveDeployProvenance {
20
20
  gitCommit?: string | null;
21
21
  gitBranch?: string | null;
22
22
  gitDirty?: boolean | null;
23
+ /**
24
+ * The target deployment's own environment (its Atlas `APP_ENV`). A CLI `-e`
25
+ * name is only a local alias (`sandbox` in an agent sandbox, or whatever
26
+ * `envs add` chose), so the CLI never supplies it.
27
+ */
23
28
  environment?: string | null;
24
29
  }
25
30
  /** Canonical default branch name for official artifact deploys. */
@@ -36,7 +41,15 @@ export declare function isActiveDeployDefaultBranch(branch: string | null | unde
36
41
  /** Recognize first-party and provisioned staging environment names. */
37
42
  export declare function isStagingEnvironment(environment: string | null | undefined): boolean;
38
43
  /**
39
- * Throw if provenance is not eligible for an official (active) deploy.
40
- * Preview callers must not invoke this.
44
+ * Throw if provenance is not eligible for an official (active) deploy to the
45
+ * deployment named by `provenance.environment`. Preview callers must not
46
+ * invoke this.
41
47
  */
42
48
  export declare function assertActiveDeployProvenance(provenance: ActiveDeployProvenance): void;
49
+ /**
50
+ * The CLI's check before it builds or uploads. Only Atlas knows whether the
51
+ * target deployment is a staging environment, so this admits `staging` and
52
+ * leaves that decision to Atlas, which repeats the full check with its own
53
+ * environment.
54
+ */
55
+ export declare function assertActiveDeploySourceBeforeUpload(provenance: Omit<ActiveDeployProvenance, 'environment'>): void;
@@ -44,11 +44,31 @@ export function isStagingEnvironment(environment) {
44
44
  const normalized = environment?.trim().toLowerCase();
45
45
  return normalized === 'staging' || normalized?.endsWith('_staging') === true || normalized?.endsWith('-staging') === true;
46
46
  }
47
+ /** The preview deploy an official-deploy refusal points at. */
48
+ function previewHint(ref) {
49
+ return `Deploy it as a preview instead: \`seq-studio artifact deploy --repo <ns>/<name> --ref ${ref} --preview\``;
50
+ }
47
51
  /**
48
- * Throw if provenance is not eligible for an official (active) deploy.
49
- * Preview callers must not invoke this.
52
+ * Throw if provenance is not eligible for an official (active) deploy to the
53
+ * deployment named by `provenance.environment`. Preview callers must not
54
+ * invoke this.
50
55
  */
51
56
  export function assertActiveDeployProvenance(provenance) {
57
+ assertOfficialSource({
58
+ provenance,
59
+ target: { staging: isStagingEnvironment(provenance.environment), environment: provenance.environment },
60
+ });
61
+ }
62
+ /**
63
+ * The CLI's check before it builds or uploads. Only Atlas knows whether the
64
+ * target deployment is a staging environment, so this admits `staging` and
65
+ * leaves that decision to Atlas, which repeats the full check with its own
66
+ * environment.
67
+ */
68
+ export function assertActiveDeploySourceBeforeUpload(provenance) {
69
+ assertOfficialSource({ provenance, target: { staging: 'decided-by-atlas' } });
70
+ }
71
+ function assertOfficialSource({ provenance, target }) {
52
72
  // Require an explicit clean source-directory recording — omitting gitDirty
53
73
  // must not pass. The local source resolver scopes this value to the
54
74
  // artifact directory, so unrelated changes elsewhere in a containing
@@ -66,12 +86,16 @@ export function assertActiveDeployProvenance(provenance) {
66
86
  '`seq-studio artifact deploy --repo <ns>/<slug>` (defaults to the repo default branch). ' +
67
87
  'Feature branches and detached HEAD (unless at the tip of `main`) are preview-only.');
68
88
  }
69
- const stagingBranchAllowed = branch === ACTIVE_DEPLOY_STAGING_BRANCH && isStagingEnvironment(provenance.environment);
70
- if (branch !== ACTIVE_DEPLOY_DEFAULT_BRANCH && !stagingBranchAllowed) {
89
+ if (branch === ACTIVE_DEPLOY_STAGING_BRANCH && target.staging === false) {
90
+ const environment = target.environment?.trim();
91
+ throw new ActiveDeployPolicyError(`The \`${ACTIVE_DEPLOY_STAGING_BRANCH}\` branch deploys officially only to a staging deployment, ` +
92
+ `and this deployment${environment ? ` (\`${environment}\`)` : ''} is not one. ` +
93
+ `Deploy \`${ACTIVE_DEPLOY_DEFAULT_BRANCH}\` here. ${previewHint(branch)}`);
94
+ }
95
+ if (branch !== ACTIVE_DEPLOY_DEFAULT_BRANCH && branch !== ACTIVE_DEPLOY_STAGING_BRANCH) {
71
96
  throw new ActiveDeployPolicyError(`Active (official) artifact deploys must use \`${ACTIVE_DEPLOY_DEFAULT_BRANCH}\` ` +
72
- `or \`${ACTIVE_DEPLOY_STAGING_BRANCH}\` for staging targets ` +
73
- `(got branch/ref \`${branch}\`). Merge via a Pull, then redeploy from \`main\`; ` +
74
- 'use a preview channel for feature-branch or dirty local work.');
97
+ `(or \`${ACTIVE_DEPLOY_STAGING_BRANCH}\` on a staging deployment); got branch/ref \`${branch}\`. ` +
98
+ `Merge it via a Pull, then redeploy from \`${ACTIVE_DEPLOY_DEFAULT_BRANCH}\`. ${previewHint(branch)}`);
75
99
  }
76
100
  if (!provenance.gitCommit?.trim()) {
77
101
  throw new ActiveDeployPolicyError('Active (official) artifact deploys require a resolved git commit on `main` ' +
package/dist/build.js CHANGED
@@ -83,6 +83,12 @@ function createPeerResolveAlias({ atlasUiPackageDir = atlasUiDir } = {}) {
83
83
  '@tanstack/react-query': reactQueryDir,
84
84
  'react-router-dom': reactRouterDomDir,
85
85
  sonner: sonnerDir,
86
+ // The CLI pins the shared SDK just as it pins ORM/Storage. Explicit subpaths
87
+ // precede the package root because Vite aliases also match prefixes.
88
+ '@sequenceholdings/platform/browser': cliRequire.resolve('@sequenceholdings/platform/browser'),
89
+ '@sequenceholdings/platform/contracts': cliRequire.resolve('@sequenceholdings/platform/contracts'),
90
+ '@sequenceholdings/platform/storage': cliRequire.resolve('@sequenceholdings/platform/storage'),
91
+ '@sequenceholdings/platform': cliRequire.resolve('@sequenceholdings/platform'),
86
92
  '@sequenceholdings/storage': cliRequire.resolve('@sequenceholdings/storage'),
87
93
  '@sequenceholdings/orm/client': cliRequire.resolve('@sequenceholdings/orm/client'),
88
94
  '@sequenceholdings/artifact-studio/offline': offlineRuntimeEntry,
package/dist/cli.js CHANGED
@@ -16,11 +16,12 @@ import { acquireDevLock } from './dev-lock.js';
16
16
  import { runWatchLoop } from './watch-loop.js';
17
17
  import { hashLinkedSources, hasLinkedAtlasUiToolchain, parseLinks, rebuildLinkedDep, resolveLinkedDeps, syncLinkedDist, } from './dev-link.js';
18
18
  import { lintArtifactSandbox, reportSandboxWarnings } from './sandbox-lint.js';
19
- import { assertActiveDeployProvenance } from './active-deploy-policy.js';
19
+ import { assertActiveDeploySourceBeforeUpload } from './active-deploy-policy.js';
20
20
  import { assertArtifactEnvironmentBindings } from './artifact-env-bindings.js';
21
21
  import { normalizeArtifactPath } from './paths.js';
22
22
  import yaml from 'js-yaml';
23
23
  import { warnOnRoleDefinitions } from './role-definitions-notice.js';
24
+ import { branchPreviewKey } from './preview-key.js';
24
25
  const COMMANDS = [
25
26
  'init', 'whoami', 'link', 'env', 'status', 'list', 'show',
26
27
  'content-readers', 'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
@@ -37,6 +38,7 @@ const USAGE = `usage:
37
38
  seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--source-env <env>] [--out dist/artifact-bundle.json]
38
39
  seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--source-env <env>] --env <env>
39
40
  seq-studio artifact deploy [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--source-env <env>] --env <env> [--skip-unchanged] [--no-create] [--project <id>]
41
+ seq-studio artifact deploy --repo <ns>/<name> --ref <branch> --preview [--key <key>] --env <env>
40
42
  seq-studio artifact dev [dir] --env <env> [--once] [--key default] [--link <pkgDir>[,<pkgDir>]] [--port <port>]
41
43
  seq-studio artifact promote <deployment-id> --env <env>
42
44
  seq-studio artifact pull <project-id> --env <env> --out <dir>
@@ -60,6 +62,15 @@ const USAGE = `usage:
60
62
  trees; feature branches and dirty source trees remain preview-only for
61
63
  deployed environments (\`artifact dev\` / preview channel).
62
64
 
65
+ A preview is a private test copy of the app. deploy --preview builds any
66
+ branch, tag, or commit and prints a link that opens it inside Atlas for people
67
+ who can edit the project; the live app and its users are unaffected. For a
68
+ static app each --key (default: the --ref name, else "default") keeps one
69
+ preview, replaced by the next deploy with that key. Each Next.js preview is
70
+ its own immutable build with its own link, so --key does not apply. A
71
+ preview is never promoted: merge the branch and deploy main (or staging) to
72
+ go live.
73
+
63
74
  Interactive --repo builds clone over smart-HTTP using your seq-studio login
64
75
  and repository read access. No PAT is needed. Headless M2M builds use JSON
65
76
  materialize and accept only platform-managed --repo sources. Use --env to
@@ -127,7 +138,7 @@ function validateKnownOptions(args) {
127
138
  show: { values: ['env'], booleans: ['json'] }, validate: { values: [], booleans: [] },
128
139
  build: { values: ['source-env', 'env', 'repo', 'git-url', 'ref', 'path', 'out'], booleans: [] },
129
140
  plan: { values: ['source-env', 'env', 'repo', 'git-url', 'ref', 'path'], booleans: [] },
130
- deploy: { values: ['source-env', 'env', 'repo', 'git-url', 'ref', 'path', 'project', 'out'], booleans: ['skip-unchanged', 'no-create'] },
141
+ deploy: { values: ['source-env', 'env', 'repo', 'git-url', 'ref', 'path', 'project', 'out', 'key'], booleans: ['skip-unchanged', 'no-create', 'preview'] },
131
142
  dev: { values: ['env', 'key', 'link', 'port'], booleans: ['once'] },
132
143
  promote: { values: ['env'], booleans: [] }, pull: { values: ['env', 'out'], booleans: [] },
133
144
  rollback: { values: ['env'], booleans: [] },
@@ -565,6 +576,7 @@ async function planCommand(args) {
565
576
  }
566
577
  }
567
578
  async function deployCommand(args) {
579
+ const preview = isPreviewDeploy(args);
568
580
  const env = await envFromArgs(args);
569
581
  const auth = await getRequiredAuth(args);
570
582
  const { token } = auth;
@@ -580,16 +592,19 @@ async function deployCommand(args) {
580
592
  try {
581
593
  // Fail closed before build / project create for deployed environments.
582
594
  // Local Atlas is a developer's own target, so it intentionally accepts
583
- // the current branch and dirty working tree.
584
- if (env.name !== 'local') {
585
- assertActiveDeployProvenance({ ...source.provenance, environment: env.name });
595
+ // the current branch and dirty working tree. A preview accepts any ref.
596
+ if (env.name !== 'local' && !preview) {
597
+ assertActiveDeploySourceBeforeUpload(source.provenance);
586
598
  }
587
599
  const manifest = await readArtifactStudioManifest(source.dir);
588
600
  // After source resolution, so --repo / --git-url deploys check the fetched manifest.
589
601
  warnOnRoleDefinitions(manifest);
590
602
  await assertArtifactEnvironmentBindings({ directory: source.dir, environment: env, token });
591
603
  if (isNextArtifactManifest(manifest)) {
592
- return await buildNextCommand({ args, source, active: true });
604
+ if (preview && args.flags.key !== undefined) {
605
+ throw new Error('--key names a static preview. Each Next.js preview is its own immutable build with its own link; omit --key.');
606
+ }
607
+ return await buildNextCommand({ args, source, active: !preview });
593
608
  }
594
609
  assertSourceEnvironmentSupportsManifest(args);
595
610
  // --skip-unchanged: hash the raw source (no build needed) and no-op when
@@ -631,6 +646,8 @@ async function deployCommand(args) {
631
646
  allowCreate: args.flags['no-create'] !== true,
632
647
  explicitProjectId,
633
648
  });
649
+ if (preview)
650
+ return await uploadPreview({ args, envUrl: env.url, token, project, result, provenance: source.provenance });
634
651
  const deployment = await uploadDeployment({ env: env.url, token, projectId: project.id, result, channel: 'active', provenance: source.provenance });
635
652
  console.log(`[seq-studio] deployed ${project.title} ${deployment.version}`);
636
653
  console.log(`${env.url}/artifact-studio/projects/${project.id}/deployments/${deployment.id}`);
@@ -640,6 +657,25 @@ async function deployCommand(args) {
640
657
  await source.cleanup();
641
658
  }
642
659
  }
660
+ /** `deploy --preview`: any ref, never the live app. --key belongs to it, --skip-unchanged does not. */
661
+ function isPreviewDeploy(args) {
662
+ const preview = args.flags.preview === true;
663
+ if (!preview && args.flags.key !== undefined)
664
+ throw new Error('--key names a preview; add --preview, or omit --key for an official deploy.');
665
+ if (preview && args.flags['skip-unchanged'] === true)
666
+ throw new Error('--skip-unchanged compares against the official deployment; it does not apply to --preview.');
667
+ return preview;
668
+ }
669
+ async function uploadPreview({ args, envUrl, token, project, result, provenance }) {
670
+ const previewKey = branchPreviewKey({ key: args.flags.key, ref: args.flags.ref });
671
+ const deployment = await uploadDeployment({
672
+ env: envUrl, token, projectId: project.id, result, channel: 'preview', previewKey, provenance,
673
+ message: `Preview of ${provenance.gitBranch ?? provenance.gitCommit?.slice(0, 12) ?? 'local source'}`,
674
+ });
675
+ console.log(`[seq-studio] preview ${deployment.version} of ${project.title} (key ${previewKey}); the live app is unchanged`);
676
+ console.log(`${envUrl}/artifact-studio/projects/${project.id}/preview?key=${encodeURIComponent(previewKey)}`);
677
+ return 0;
678
+ }
643
679
  async function buildNextCommand({ args, source, active }) {
644
680
  const env = await envFromArgs(args);
645
681
  const token = await getRequiredToken(args);
@@ -650,7 +686,7 @@ async function buildNextCommand({ args, source, active }) {
650
686
  const spec = parseSourceSpec(args);
651
687
  const repository = nextSourceRepository({ spec, root: source.dir });
652
688
  if (active) {
653
- assertActiveDeployProvenance({ ...source.provenance, environment: env.name });
689
+ assertActiveDeploySourceBeforeUpload(source.provenance);
654
690
  }
655
691
  const sourceEnvironment = sourceEnvironmentFromArgs(args);
656
692
  const sourceToken = sourceEnvironment ? (await getSourceAuth(args)).token : token;
@@ -77,6 +77,11 @@ export declare const artifactStudioStaticManifestSchema: z.ZodObject<{
77
77
  functions: z.ZodDefault<z.ZodObject<{
78
78
  invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
79
79
  }, z.core.$strip>>;
80
+ processes: z.ZodDefault<z.ZodObject<{
81
+ start: z.ZodDefault<z.ZodArray<z.ZodString>>;
82
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
83
+ act: z.ZodDefault<z.ZodArray<z.ZodString>>;
84
+ }, z.core.$strip>>;
80
85
  allow_downloads: z.ZodDefault<z.ZodBoolean>;
81
86
  allow_popups: z.ZodDefault<z.ZodBoolean>;
82
87
  roles: z.ZodOptional<z.ZodObject<{
@@ -3,6 +3,7 @@ import { ontologyContractSchema } from '@sequenceholdings/orm/canonical';
3
3
  import { z } from 'zod';
4
4
  import { capabilityPinsSchema, customRolesBlockSchema, validateCapabilityPins, } from './manifest-custom-roles.js';
5
5
  import { normalizeArtifactPath } from './paths.js';
6
+ import { latticeProcessRef } from './manifest-next-reach.js';
6
7
  export { CUSTOM_CAPABILITY_PIN_RE, CUSTOM_ROLE_KEY_RE, capabilityPinsSchema, customRolesBlockSchema, customRolesDeclared, parseCapabilityPin, validateCapabilityPins, } from './manifest-custom-roles.js';
7
8
  const apiCapabilityPathSchema = z.string().min(1).refine((path) => path.startsWith('/api/') && path !== '/api/' && path !== '/api/*', 'API capability paths must be scoped below /api/');
8
9
  const apiCapabilitiesSchema = z.object({
@@ -91,6 +92,18 @@ export const artifactStudioStaticManifestSchema = z.object({
91
92
  functions: z.object({
92
93
  invoke: z.array(z.string().min(1)).default([]),
93
94
  }).default({ invoke: [] }),
95
+ /**
96
+ * Lattice processes `createArtifactProcesses()` may reach, as in a Next.js
97
+ * manifest: `start` runs of, `read` runs, templates and the user's own steps
98
+ * of, and `act` on (complete or reassign human steps, cancel, share runs).
99
+ * Exact process ids; Atlas checks them against the deployed manifest, then
100
+ * the user's own Invoker / Viewer / Advancer / Owner grants decide.
101
+ */
102
+ processes: z.object({
103
+ start: z.array(latticeProcessRef).max(256).default([]),
104
+ read: z.array(latticeProcessRef).max(256).default([]),
105
+ act: z.array(latticeProcessRef).max(256).default([]),
106
+ }).default({ start: [], read: [], act: [] }),
94
107
  /** Opt-in: enables iframe `allow-downloads`. Default false. */
95
108
  allow_downloads: z.boolean().default(false),
96
109
  /**
@@ -117,6 +130,7 @@ export const artifactStudioStaticManifestSchema = z.object({
117
130
  data: { read: [], write: [] },
118
131
  api: { read: [], write: [] },
119
132
  functions: { invoke: [] },
133
+ processes: { start: [], read: [], act: [] },
120
134
  allow_downloads: false,
121
135
  allow_popups: false,
122
136
  uses: [],
@@ -48,6 +48,11 @@ export declare const artifactStudioManifestSchema: z.ZodUnion<readonly [z.ZodObj
48
48
  functions: z.ZodDefault<z.ZodObject<{
49
49
  invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
50
50
  }, z.core.$strip>>;
51
+ processes: z.ZodDefault<z.ZodObject<{
52
+ start: z.ZodDefault<z.ZodArray<z.ZodString>>;
53
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
54
+ act: z.ZodDefault<z.ZodArray<z.ZodString>>;
55
+ }, z.core.$strip>>;
51
56
  allow_downloads: z.ZodDefault<z.ZodBoolean>;
52
57
  allow_popups: z.ZodDefault<z.ZodBoolean>;
53
58
  roles: z.ZodOptional<z.ZodObject<{