@sequenceholdings/studio-cli 0.1.13 → 0.1.21

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 (63) hide show
  1. package/README.md +258 -38
  2. package/dist/agents/apply-chunks.d.ts +13 -0
  3. package/dist/agents/apply-chunks.js +43 -0
  4. package/dist/agents/commands.d.ts +10 -0
  5. package/dist/agents/commands.js +218 -0
  6. package/dist/agents/scaffold.d.ts +2 -0
  7. package/dist/agents/scaffold.js +77 -0
  8. package/dist/agents/source.d.ts +18 -0
  9. package/dist/agents/source.js +121 -0
  10. package/dist/artifact/delegate.d.ts +2 -2
  11. package/dist/artifact/delegate.js +31 -73
  12. package/dist/atlas-client.js +52 -37
  13. package/dist/auth-cmds/commands.d.ts +1 -1
  14. package/dist/auth-cmds/commands.js +12 -7
  15. package/dist/auth.d.ts +104 -24
  16. package/dist/auth.js +456 -94
  17. package/dist/config.d.ts +3 -3
  18. package/dist/config.js +18 -13
  19. package/dist/env-catalog.js +13 -3
  20. package/dist/env-flags.d.ts +2 -0
  21. package/dist/env-flags.js +2 -0
  22. package/dist/env-registry.d.ts +27 -0
  23. package/dist/env-registry.js +204 -0
  24. package/dist/envs/commands.d.ts +1 -1
  25. package/dist/envs/commands.js +41 -3
  26. package/dist/file-lock.d.ts +5 -0
  27. package/dist/file-lock.js +187 -0
  28. package/dist/functions/commands.d.ts +10 -10
  29. package/dist/functions/commands.js +87 -53
  30. package/dist/functions/manifest.d.ts +1 -0
  31. package/dist/functions/manifest.js +36 -0
  32. package/dist/functions/source-selection.d.ts +24 -0
  33. package/dist/functions/source-selection.js +67 -0
  34. package/dist/login.d.ts +8 -3
  35. package/dist/login.js +46 -34
  36. package/dist/main.d.ts +3 -1
  37. package/dist/main.js +41 -12
  38. package/dist/orm/delegate.js +25 -7
  39. package/dist/pat-hints.js +2 -2
  40. package/dist/pipeline/commands.d.ts +58 -0
  41. package/dist/pipeline/commands.js +330 -0
  42. package/dist/pipeline/lifecycle.d.ts +58 -0
  43. package/dist/pipeline/lifecycle.js +348 -0
  44. package/dist/pipeline/pinning.d.ts +5 -0
  45. package/dist/pipeline/pinning.js +9 -0
  46. package/dist/pipeline/templates.d.ts +11 -0
  47. package/dist/pipeline/templates.js +166 -0
  48. package/dist/process/build.d.ts +4 -0
  49. package/dist/process/build.js +33 -2
  50. package/dist/process/codegen.js +19 -1
  51. package/dist/process/commands.js +97 -47
  52. package/dist/process/compiler-subprocess.d.ts +29 -0
  53. package/dist/process/compiler-subprocess.js +99 -0
  54. package/dist/process/compiler-worker.d.ts +1 -0
  55. package/dist/process/compiler-worker.js +38 -0
  56. package/dist/process/lint.d.ts +8 -0
  57. package/dist/process/lint.js +84 -29
  58. package/dist/process/repo-install.js +18 -2
  59. package/dist/repos/commands.d.ts +1 -1
  60. package/dist/repos/commands.js +17 -12
  61. package/dist/secrets/commands.d.ts +1 -1
  62. package/dist/secrets/commands.js +18 -18
  63. package/package.json +12 -5
@@ -0,0 +1,218 @@
1
+ import { getJson, postJson } from '../atlas-client.js';
2
+ import { readConfig } from '../config.js';
3
+ import { applyInChunks } from './apply-chunks.js';
4
+ import { confirmYes } from '../prompt.js';
5
+ import { buildContext, clientOptions, flagBool, printError, requestedEnvironment, } from '../functions/commands.js';
6
+ import { compileAgentSource as compileSource, materializeAgentSource as materialize, } from './source.js';
7
+ import { agentsInitCommand } from './scaffold.js';
8
+ export { agentsInitCommand };
9
+ const LOG = '[seq-studio]';
10
+ export async function agentsValidateCommand(args) {
11
+ const { source } = await materialize({ args, requireEnvironment: false });
12
+ try {
13
+ const target = typeof args.flags.target === 'string' ? args.flags.target : undefined;
14
+ const bundle = await compileSource({
15
+ directory: source.dir,
16
+ targetEnvironment: target,
17
+ deployEnvironments: await deployEnvironmentNames(),
18
+ });
19
+ if (bundle.definitions.length === 0) {
20
+ console.error(`${LOG} no named agent.ts definitions found`);
21
+ return 1;
22
+ }
23
+ console.log(`${LOG} valid: ${bundle.definitions.length} definition${bundle.definitions.length === 1 ? '' : 's'}, bundle ${bundle.hash}`);
24
+ return 0;
25
+ }
26
+ finally {
27
+ await source.cleanup();
28
+ }
29
+ }
30
+ async function deploymentBundle({ args, source, context, }) {
31
+ const target = (typeof args.flags.target === 'string' ? args.flags.target : undefined) ??
32
+ context.env.name;
33
+ return compileSource({
34
+ directory: source.dir,
35
+ targetEnvironment: target,
36
+ deployEnvironments: await deployEnvironmentNames(),
37
+ });
38
+ }
39
+ /**
40
+ * Names the CLI can actually deploy to. A target outside both this set and the
41
+ * manifest's own vocabulary is a typo, not a selection.
42
+ */
43
+ async function deployEnvironmentNames() {
44
+ try {
45
+ return Object.keys((await readConfig()).envs);
46
+ }
47
+ catch {
48
+ // Never let an unreadable config block a deploy — the guard is a safety
49
+ // net, and the manifest's own vocabulary still constrains the target.
50
+ return [];
51
+ }
52
+ }
53
+ function printPlan(plan) {
54
+ for (const entry of plan.entries) {
55
+ const marker = entry.action === 'create' ? '+' : entry.action === 'update' ? '~' : '=';
56
+ // Name the fields an update would rewrite, so an operator can tell an
57
+ // intended edit from unexpected drift before confirming an apply.
58
+ const fields = entry.changedFields?.length
59
+ ? `: ${entry.changedFields.join(', ')}`
60
+ : '';
61
+ console.log(` ${marker} ${entry.name} (${entry.id}) ${entry.action}${fields}`);
62
+ }
63
+ console.log(`${LOG} plan: ${plan.summary.creates} create, ${plan.summary.updates} update, ${plan.summary.unchanged} unchanged`);
64
+ }
65
+ export async function agentsPlanCommand(args) {
66
+ const environment = requestedEnvironment(args);
67
+ if (!environment) {
68
+ const { source } = await materialize({ args, requireEnvironment: false });
69
+ try {
70
+ // Honor --target the same way validate does. Without this, offline plan
71
+ // silently compiles the unfiltered manifest set even when the caller
72
+ // asked for a specific environment's overrides.
73
+ const target = typeof args.flags.target === 'string' ? args.flags.target : undefined;
74
+ const bundle = await compileSource({
75
+ directory: source.dir,
76
+ targetEnvironment: target,
77
+ deployEnvironments: await deployEnvironmentNames(),
78
+ });
79
+ console.log(`${LOG} offline plan: ${bundle.definitions.length} valid definition${bundle.definitions.length === 1 ? '' : 's'}, bundle ${bundle.hash}`);
80
+ console.log(`${LOG} pass -e <env> for create/update/unchanged live diff`);
81
+ return bundle.definitions.length > 0 ? 0 : 1;
82
+ }
83
+ finally {
84
+ await source.cleanup();
85
+ }
86
+ }
87
+ const { source, context } = await materialize({
88
+ args,
89
+ requireEnvironment: true,
90
+ });
91
+ if (!context)
92
+ throw new Error('Missing deployment context');
93
+ try {
94
+ const bundle = await deploymentBundle({ args, source, context });
95
+ const plan = await postJson({
96
+ ...clientOptions(context),
97
+ path: '/api/agents/deploy/plan',
98
+ body: {
99
+ definitions: bundle.definitions,
100
+ bundleHash: bundle.hash,
101
+ source: source.provenance.gitCommit ?? 'local',
102
+ },
103
+ });
104
+ printPlan(plan);
105
+ return 0;
106
+ }
107
+ finally {
108
+ await source.cleanup();
109
+ }
110
+ }
111
+ export async function agentsApplyCommand(args) {
112
+ const { source, context } = await materialize({
113
+ args,
114
+ requireEnvironment: true,
115
+ });
116
+ if (!context)
117
+ throw new Error('Missing deployment context');
118
+ try {
119
+ const bundle = await deploymentBundle({ args, source, context });
120
+ const plan = await postJson({
121
+ ...clientOptions(context),
122
+ path: '/api/agents/deploy/plan',
123
+ body: {
124
+ definitions: bundle.definitions,
125
+ bundleHash: bundle.hash,
126
+ source: source.provenance.gitCommit ?? 'local',
127
+ },
128
+ });
129
+ printPlan(plan);
130
+ const preview = [
131
+ `${LOG} apply ${bundle.definitions.length} agent definitions to ${context.env.name}`,
132
+ `${LOG} bundle ${bundle.hash}`,
133
+ `${LOG} no agents will be deleted`,
134
+ ];
135
+ if (!(await confirmYes({ preview, confirmed: flagBool(args.flags, 'yes') }))) {
136
+ return 1;
137
+ }
138
+ const summary = await applyInChunks({
139
+ context,
140
+ definitions: bundle.definitions,
141
+ source: source.provenance.gitCommit ?? 'local',
142
+ });
143
+ console.log(`${LOG} applied: ${summary.created} created, ${summary.updated} updated, ${summary.unchanged} unchanged`);
144
+ return 0;
145
+ }
146
+ finally {
147
+ await source.cleanup();
148
+ }
149
+ }
150
+ export async function agentsListCommand(args) {
151
+ const context = await buildContext(args);
152
+ const agents = await getJson({
153
+ ...clientOptions(context),
154
+ path: '/api/agents/agents?view=list',
155
+ });
156
+ for (const agent of agents) {
157
+ console.log(`${agent.id} ${agent.name} managed=${agent.managedBy ?? 'user'}`);
158
+ }
159
+ return 0;
160
+ }
161
+ export async function agentsShowCommand(args) {
162
+ const id = args.positional[0];
163
+ if (!id) {
164
+ console.error('usage: seq-studio agents show <id> -e <env>');
165
+ return 1;
166
+ }
167
+ const context = await buildContext(args);
168
+ const agent = await getJson({
169
+ ...clientOptions(context),
170
+ path: `/api/agents/agents/${encodeURIComponent(id)}`,
171
+ });
172
+ console.log(JSON.stringify(agent, null, 2));
173
+ return 0;
174
+ }
175
+ export const AGENTS_USAGE = `usage:
176
+ seq-studio agents init <dir> scaffold a typed agent
177
+ seq-studio agents validate [--dir d] [--target app] offline compile + validation
178
+ seq-studio agents plan [--dir d] [-e <env>] offline bundle plan or live diff
179
+ seq-studio agents apply [--dir d] -e <env> [--yes] apply creates/updates; never deletes
180
+ seq-studio agents list -e <env> list visible agents
181
+ seq-studio agents show <id> -e <env> show one agent
182
+
183
+ Source: local --dir (default .), --repo agents/<name>, or --git-url <url>.
184
+ Use --ref for remote sources. --target selects the deployment APP_ENV when it
185
+ differs from the CLI environment alias (notably OpCo registrations).
186
+ `;
187
+ export async function runAgentsCommand(sub, args) {
188
+ try {
189
+ switch (sub) {
190
+ case 'init':
191
+ return await agentsInitCommand(args);
192
+ case 'validate':
193
+ return await agentsValidateCommand(args);
194
+ case 'plan':
195
+ return await agentsPlanCommand(args);
196
+ case 'apply':
197
+ return await agentsApplyCommand(args);
198
+ case 'list':
199
+ return await agentsListCommand(args);
200
+ case 'show':
201
+ return await agentsShowCommand(args);
202
+ case 'help':
203
+ case '--help':
204
+ case '-h':
205
+ case undefined:
206
+ console.log(AGENTS_USAGE);
207
+ return sub ? 0 : 1;
208
+ default:
209
+ console.error(`unknown agents command: ${sub}`);
210
+ console.error(AGENTS_USAGE);
211
+ return 1;
212
+ }
213
+ }
214
+ catch (error) {
215
+ printError(error);
216
+ return 1;
217
+ }
218
+ }
@@ -0,0 +1,2 @@
1
+ import type { ParsedArgs } from '../process/commands.js';
2
+ export declare function agentsInitCommand(args: ParsedArgs): Promise<number>;
@@ -0,0 +1,77 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { basename, join, resolve } from 'node:path';
5
+ import { currentVersion } from '../update-check.js';
6
+ const LOG = '[seq-studio]';
7
+ const scaffoldSource = ({ id, name, }) => `import { defineAgent } from '@sequenceholdings/agent-spec'
8
+
9
+ export const agent = defineAgent({
10
+ id: '${id}',
11
+ name: '${name}',
12
+ model: {
13
+ provider: 'anthropic',
14
+ name: 'claude-sonnet-4-6',
15
+ },
16
+ systemPrompt: 'Describe the agent role and operating instructions.',
17
+ })
18
+ `;
19
+ export async function agentsInitCommand(args) {
20
+ const target = args.positional[0];
21
+ if (!target) {
22
+ console.error('usage: seq-studio agents init <dir>');
23
+ return 1;
24
+ }
25
+ const directory = resolve(target);
26
+ const file = join(directory, 'agent.ts');
27
+ if (existsSync(file)) {
28
+ console.error(`${LOG} ${file} already exists`);
29
+ return 1;
30
+ }
31
+ const name = basename(directory)
32
+ .split(/[-_]+/)
33
+ .filter(Boolean)
34
+ .map((part) => `${part[0]?.toUpperCase() ?? ''}${part.slice(1)}`)
35
+ .join(' ');
36
+ await mkdir(directory, { recursive: true });
37
+ await writeFile(file, scaffoldSource({ id: randomBytes(12).toString('hex'), name }));
38
+ await writeFile(join(directory, 'package.json'), `${JSON.stringify({
39
+ name: basename(directory),
40
+ version: '0.0.1',
41
+ private: true,
42
+ type: 'module',
43
+ scripts: { validate: 'seq-studio agents validate' },
44
+ dependencies: { '@sequenceholdings/agent-spec': '^0.1.0' },
45
+ devDependencies: {
46
+ '@sequenceholdings/studio-cli': `^${currentVersion()}`,
47
+ },
48
+ }, null, 2)}\n`);
49
+ await writeFile(join(directory, 'tsconfig.json'), `${JSON.stringify({
50
+ compilerOptions: {
51
+ target: 'ES2022',
52
+ module: 'NodeNext',
53
+ moduleResolution: 'NodeNext',
54
+ strict: true,
55
+ noEmit: true,
56
+ },
57
+ include: ['agent.ts'],
58
+ }, null, 2)}\n`);
59
+ await writeFile(join(directory, 'pnpm-workspace.yaml'), `packages:
60
+ - '.'
61
+
62
+ minimumReleaseAge: 10080
63
+ minimumReleaseAgeExclude:
64
+ - '@sequenceholdings/agent-spec'
65
+ - '@sequenceholdings/atlas-ui'
66
+ - '@sequenceholdings/lattice-form-renderer'
67
+ - '@sequenceholdings/artifact-studio'
68
+ - '@sequenceholdings/lattice'
69
+ - '@sequenceholdings/studio-cli'
70
+ strictDepBuilds: true
71
+ allowBuilds:
72
+ esbuild: true
73
+ `);
74
+ console.log(`${LOG} scaffolded typed agent in ${directory}`);
75
+ console.log('Next: pnpm install && seq-studio agents validate');
76
+ return 0;
77
+ }
@@ -0,0 +1,18 @@
1
+ import { type CompiledAgentBundle } from '@sequenceholdings/agent-spec/compiler';
2
+ import { type ResolvedSource, type SourceSpec } from '@sequenceholdings/artifact-studio/source-resolver';
3
+ import type { ParsedArgs } from '../process/commands.js';
4
+ import { type CommandContext } from '../functions/commands.js';
5
+ export declare function materializeAgentSource({ args, requireEnvironment, }: {
6
+ args: ParsedArgs;
7
+ requireEnvironment: boolean;
8
+ }): Promise<{
9
+ spec: SourceSpec;
10
+ source: ResolvedSource;
11
+ context: CommandContext | null;
12
+ }>;
13
+ export declare function compileAgentSource({ directory, targetEnvironment, deployEnvironments, }: {
14
+ directory: string;
15
+ targetEnvironment?: string;
16
+ /** Registered deployment environments, so a real env absent from the manifest is not read as a typo. */
17
+ deployEnvironments?: readonly string[];
18
+ }): Promise<CompiledAgentBundle>;
@@ -0,0 +1,121 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { hashAgentBundle, } from '@sequenceholdings/agent-spec';
5
+ import { agentDeployManifestSchema, assertKnownTargetEnvironment, compileAgentDirectory, selectAgentEntries, selectionStats, } from '@sequenceholdings/agent-spec/compiler';
6
+ import { parseSourceSpec, resolveArtifactSource, } from '@sequenceholdings/artifact-studio/source-resolver';
7
+ import { buildContext, clientOptions, requestedEnvironment, } from '../functions/commands.js';
8
+ const MANIFEST = 'deploy-manifest.json';
9
+ function sourceSpec(args) {
10
+ const dir = typeof args.flags.dir === 'string' ? args.flags.dir : undefined;
11
+ if (dir &&
12
+ (args.flags.repo !== undefined || args.flags['git-url'] !== undefined)) {
13
+ throw new Error('--dir cannot be combined with --repo / --git-url.');
14
+ }
15
+ return parseSourceSpec({
16
+ positional: dir ? [dir] : [],
17
+ flags: args.flags,
18
+ });
19
+ }
20
+ function sourceOptions(context) {
21
+ return {
22
+ ...(context.authMode ? { authMode: context.authMode } : {}),
23
+ ...clientOptions(context),
24
+ };
25
+ }
26
+ export async function materializeAgentSource({ args, requireEnvironment, }) {
27
+ const spec = sourceSpec(args);
28
+ const needsAuth = spec.kind !== 'local';
29
+ if (requireEnvironment && !requestedEnvironment(args)) {
30
+ throw new Error('Network commands require an explicit -e/--env.');
31
+ }
32
+ const context = needsAuth || requireEnvironment ? await buildContext(args) : null;
33
+ const source = await resolveArtifactSource(spec, context ? sourceOptions(context) : {});
34
+ return { spec, source, context };
35
+ }
36
+ async function readManifest(directory) {
37
+ const path = join(directory, MANIFEST);
38
+ if (!existsSync(path))
39
+ return null;
40
+ return agentDeployManifestSchema.parse(JSON.parse(await readFile(path, 'utf8')));
41
+ }
42
+ /**
43
+ * Refuse to deploy when a compiled export's id does not match the id the
44
+ * manifest keyed the environment override on. Without that check a mis-labeled
45
+ * agent.ts would silently deploy under a different identity and defeat tenant
46
+ * overrides.
47
+ *
48
+ * Every selected path is parsed in one pass. Compiling them one file at a
49
+ * time previously re-parsed each agent separately and dominated validate
50
+ * latency on a large fleet — paid again on plan, and twice more on apply,
51
+ * since apply plans before it writes.
52
+ */
53
+ async function compileManifestEntries({ directory, entries, allowDuplicateIds = false, }) {
54
+ if (entries.length === 0) {
55
+ return { definitions: [], files: [], hash: hashAgentBundle({ definitions: [] }), sources: [] };
56
+ }
57
+ const compiled = await compileAgentDirectory({
58
+ rootDir: directory,
59
+ filePaths: entries.map((entry) => entry.path),
60
+ allowDuplicateIds,
61
+ });
62
+ const byFile = new Map();
63
+ for (const source of compiled.sources) {
64
+ const list = byFile.get(source.file) ?? [];
65
+ list.push(source.definition);
66
+ byFile.set(source.file, list);
67
+ }
68
+ const definitions = [];
69
+ for (const entry of entries) {
70
+ const exported = byFile.get(entry.path) ?? [];
71
+ if (exported.length !== 1) {
72
+ throw new Error(`Expected exactly one agent export in ${entry.path}, found ${exported.length}`);
73
+ }
74
+ const definition = exported[0];
75
+ if (!definition) {
76
+ throw new Error(`Expected exactly one agent export in ${entry.path}, found 0`);
77
+ }
78
+ if (definition.id !== entry.id) {
79
+ throw new Error(`Manifest id "${entry.id}" for ${entry.path} does not match exported id "${definition.id}"`);
80
+ }
81
+ definitions.push(definition);
82
+ }
83
+ return {
84
+ definitions,
85
+ files: compiled.files,
86
+ hash: hashAgentBundle({ definitions }),
87
+ sources: compiled.sources,
88
+ };
89
+ }
90
+ export async function compileAgentSource({ directory, targetEnvironment, deployEnvironments = [], }) {
91
+ const manifest = await readManifest(directory);
92
+ if (!manifest)
93
+ return compileAgentDirectory({ rootDir: directory });
94
+ if (targetEnvironment !== undefined) {
95
+ assertKnownTargetEnvironment({
96
+ manifest,
97
+ environment: targetEnvironment,
98
+ deployEnvironments,
99
+ });
100
+ // Selection is silent, so a target that matches nothing looks identical to
101
+ // a full deploy. Name the drop before the operator confirms an apply.
102
+ const { selected, skipped, total } = selectionStats({
103
+ manifest,
104
+ environment: targetEnvironment,
105
+ });
106
+ if (skipped > 0) {
107
+ console.log(`[seq-studio] target ${targetEnvironment}: ${selected} of ${total} agents selected, ${skipped} scoped to other environments`);
108
+ }
109
+ }
110
+ // With a target: last-wins by id (tenant overrides). Without: every distinct
111
+ // path, so validate still compiles override sources that lose a collapse.
112
+ const entries = selectAgentEntries({
113
+ manifest,
114
+ environment: targetEnvironment,
115
+ });
116
+ return compileManifestEntries({
117
+ directory,
118
+ entries,
119
+ allowDuplicateIds: targetEnvironment === undefined,
120
+ });
121
+ }
@@ -7,8 +7,8 @@
7
7
  * 1. Resolve `--env` from `~/.config/lattice/config.toml`.
8
8
  * 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
9
9
  * uses the resolved URL (instead of its own built-in env map).
10
- * 3. Set `ARTIFACT_STUDIO_TOKEN` from the seqapi token file so auth
11
- * is shared with the rest of `seq-studio`.
10
+ * 3. Install an internal token provider so auth is shared with the rest of
11
+ * `seq-studio` without exposing the bearer in argv or environment.
12
12
  * 4. Forward all remaining argv to `runCli`.
13
13
  */
14
14
  export declare function runArtifactCommand(sub: string | undefined, rest: string[]): Promise<number>;
@@ -7,11 +7,11 @@
7
7
  * 1. Resolve `--env` from `~/.config/lattice/config.toml`.
8
8
  * 2. Set `ARTIFACT_STUDIO_BASE_URL` so artifact-studio's API client
9
9
  * uses the resolved URL (instead of its own built-in env map).
10
- * 3. Set `ARTIFACT_STUDIO_TOKEN` from the seqapi token file so auth
11
- * is shared with the rest of `seq-studio`.
10
+ * 3. Install an internal token provider so auth is shared with the rest of
11
+ * `seq-studio` without exposing the bearer in argv or environment.
12
12
  * 4. Forward all remaining argv to `runCli`.
13
13
  */
14
- import { getAccessToken, M2mTokenError, tryGetAccessToken } from '../auth.js';
14
+ import { getAccessTokenWithMode, M2mTokenError, tryGetAccessTokenWithMode, } from '../auth.js';
15
15
  import { readConfig, resolveEnvWithDiscovery } from '../config.js';
16
16
  import { fetchCatalog, readCachedCatalog } from '../env-catalog.js';
17
17
  import { normalizeShortEnvFlag, readEnvFromArgv } from '../env-flags.js';
@@ -36,11 +36,13 @@ const ARTIFACT_USAGE = `usage:
36
36
  discovered after you authenticate).
37
37
 
38
38
  Source for build/plan/deploy: a local [dir] (default), a platform git-service
39
- repo (--repo <ns>/<name>), or any git URL (--git-url <url>). --ref selects a
39
+ repo (--repo <ns>/<name>), or a public HTTPS git URL (--git-url <url>). --ref selects a
40
40
  branch/tag/commit (default: the repo's default branch).
41
41
 
42
- --repo clones over smart-HTTP and requires ATLAS_GIT_PAT (a repo:read PAT —
43
- seq-studio auth pat create, or Atlas → Settings → Tokens).
42
+ Interactive --repo builds clone over smart-HTTP and require ATLAS_GIT_PAT
43
+ (a repo:read PAT — seq-studio auth pat create, or Atlas → Settings → Tokens).
44
+ Headless M2M builds use JSON materialize and accept only platform-managed
45
+ --repo sources.
44
46
 
45
47
  Per-PR preview environments (https://studio-atlas-git-<slug>.preview.seqholdings.com):
46
48
  -e preview:<branch-or-slug> compute the preview host from a branch name
@@ -53,23 +55,20 @@ const ARTIFACT_USAGE = `usage:
53
55
  Authenticate with: seq-studio login
54
56
  `;
55
57
  export async function runArtifactCommand(sub, rest) {
58
+ if (rest.some((arg) => arg === '--token' || arg.startsWith('--token='))) {
59
+ console.error('seq-studio artifact does not accept bearer tokens via --token. ' +
60
+ 'Use `seq-studio login` interactively or configure the realm-specific ' +
61
+ 'AUTH0_M2M_CLIENT_SECRET for headless automation.');
62
+ return 1;
63
+ }
64
+ // Never pass a legacy bearer environment variable into the embedded package.
65
+ delete process.env['ARTIFACT_STUDIO_TOKEN'];
56
66
  // Older artifact-studio versions exposed nested login/logout commands backed
57
67
  // by a separate token file. Keep those commands working, but route them to
58
68
  // seq-studio's shared token so every namespace uses the same identity.
59
69
  if (sub === 'login' || sub === 'logout') {
60
70
  if (rest.length > 0) {
61
- if (sub === 'login' && rest.includes('--token')) {
62
- // The legacy `artifact login --token <jwt>` persisted a bearer to
63
- // artifact-studio's own token file — a store this unification retires.
64
- console.error('seq-studio artifact login no longer stores a bearer token.\n' +
65
- 'Scripted/headless options:\n' +
66
- ' - pass --token <jwt> directly to the artifact command (deploy/plan/whoami/...)\n' +
67
- ' - export ARTIFACT_STUDIO_TOKEN=<jwt> for the session\n' +
68
- ' - set AUTH0_M2M_CLIENT_SECRET for service-account (M2M) auth in CI');
69
- }
70
- else {
71
- console.error(`seq-studio artifact ${sub} does not accept arguments.`);
72
- }
71
+ console.error(`seq-studio artifact ${sub} does not accept arguments.`);
73
72
  return 1;
74
73
  }
75
74
  const auth = await import('../login.js');
@@ -133,51 +132,22 @@ export async function runArtifactCommand(sub, rest) {
133
132
  // without an explicit --env flag — the stored `.artifact-studio/config.json`
134
133
  // defaultEnv, `artifact env set <name>` — would otherwise fall back to
135
134
  // artifact-studio's built-in local-only map and fail on deployed envs.
136
- const { envs } = await readConfig();
137
- process.env['ARTIFACT_STUDIO_ENV_URLS'] = JSON.stringify(Object.fromEntries(Object.entries(envs).map(([name, { url }]) => [name, url])));
138
- // An explicit `--token <jwt>` is the manual escape hatch and must win over
139
- // everything, including a configured-but-failing M2M credential (which
140
- // `tryGetAccessToken({ failClosedForM2m: true })` would otherwise turn into
141
- // an abort before argv ever reaches artifact-studio). artifact-studio's
142
- // `getOptionalToken` checks `flags.token` first, so when it's present we
143
- // skip shared-token resolution entirely. Only the two-token `--token <jwt>`
144
- // form counts: artifact-studio's parser does not split `--token=<jwt>`.
145
- const hasExplicitToken = hasTokenFlag(argvForCli);
135
+ const routingConfig = await readConfig();
136
+ process.env['ARTIFACT_STUDIO_ENV_URLS'] = JSON.stringify(Object.fromEntries(Object.entries(routingConfig.envs).map(([name, { url }]) => [name, url])));
146
137
  // Lazy import so `process` / `doctor` commands don't pull in
147
138
  // artifact-studio's vite/react/tailwind dependency graph.
148
- const { runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
149
- if (!hasExplicitToken) {
150
- // Share the seqapi token. `tryGetAccessToken` resolves an M2M
151
- // service-account token when AUTH0_M2M_CLIENT_SECRET is set (headless /
152
- // CI / cloud-agent path) and otherwise the cached interactive user
153
- // token (see ../auth.ts). If neither is available, artifact-studio
154
- // commands that need a token surface their own error — we don't force
155
- // `seq-studio login` here because some commands (init, validate, build)
156
- // work offline.
157
- const token = await tryGetAccessToken({ failClosedForM2m: true });
158
- if (token) {
159
- process.env['ARTIFACT_STUDIO_TOKEN'] = token;
160
- }
161
- // The ARTIFACT_STUDIO_TOKEN env var above is captured once and never
162
- // refreshes, so long-running commands (notably `artifact dev`) would start
163
- // failing with "Authentication failed" once the initial token's TTL
164
- // elapses. Hand artifact-studio a refreshing source — getAccessToken()
165
- // mints a fresh access token via the Auth0 refresh grant when the cached
166
- // one is near expiry — so a watch session survives indefinitely.
167
- setTokenProvider(async () => {
168
- if (process.env.AUTH0_M2M_CLIENT_SECRET?.trim()) {
169
- // Fail closed for configured M2M failures so headless deploys never
170
- // silently fall back to another cached identity.
171
- return await getAccessToken();
172
- }
173
- try {
174
- return await getAccessToken();
175
- }
176
- catch {
177
- return null;
178
- }
179
- });
180
- }
139
+ const { getConfiguredDefaultEnv, runCli: runArtifactStudio, setTokenProvider } = await import('@sequenceholdings/artifact-studio/cli');
140
+ const authEnvName = resolved?.name ?? (await getConfiguredDefaultEnv());
141
+ const targetUrl = resolved?.url ??
142
+ process.env['ARTIFACT_STUDIO_BASE_URL']?.trim() ??
143
+ (authEnvName ? routingConfig.envs[authEnvName]?.url : undefined);
144
+ setTokenProvider(({ allowInteractiveLogin }) => allowInteractiveLogin
145
+ ? getAccessTokenWithMode({ env: authEnvName, targetUrl })
146
+ : tryGetAccessTokenWithMode({
147
+ failClosedForM2m: true,
148
+ env: authEnvName,
149
+ targetUrl,
150
+ }));
181
151
  return runArtifactStudio([sub, ...argvForCli]);
182
152
  }
183
153
  /**
@@ -240,18 +210,6 @@ export function extractPreviewFlags(argv) {
240
210
  }
241
211
  return { rest, prNumber, envUrl };
242
212
  }
243
- /**
244
- * True when argv carries an explicit two-token `--token <jwt>` that
245
- * artifact-studio's parser will bind to `flags.token`. `--token=<jwt>` is
246
- * excluded because that parser stores it as a stray flag and never binds it.
247
- */
248
- function hasTokenFlag(argv) {
249
- const index = argv.indexOf('--token');
250
- if (index === -1)
251
- return false;
252
- const value = argv[index + 1];
253
- return value !== undefined && !value.startsWith('--');
254
- }
255
213
  function splitInlineValue(arg) {
256
214
  const eq = arg.indexOf('=');
257
215
  if (arg.startsWith('--') && eq !== -1)