@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.
- package/README.md +258 -38
- package/dist/agents/apply-chunks.d.ts +13 -0
- package/dist/agents/apply-chunks.js +43 -0
- package/dist/agents/commands.d.ts +10 -0
- package/dist/agents/commands.js +218 -0
- package/dist/agents/scaffold.d.ts +2 -0
- package/dist/agents/scaffold.js +77 -0
- package/dist/agents/source.d.ts +18 -0
- package/dist/agents/source.js +121 -0
- package/dist/artifact/delegate.d.ts +2 -2
- package/dist/artifact/delegate.js +31 -73
- package/dist/atlas-client.js +52 -37
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +12 -7
- package/dist/auth.d.ts +104 -24
- package/dist/auth.js +456 -94
- package/dist/config.d.ts +3 -3
- package/dist/config.js +18 -13
- package/dist/env-catalog.js +13 -3
- package/dist/env-flags.d.ts +2 -0
- package/dist/env-flags.js +2 -0
- package/dist/env-registry.d.ts +27 -0
- package/dist/env-registry.js +204 -0
- package/dist/envs/commands.d.ts +1 -1
- package/dist/envs/commands.js +41 -3
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.js +187 -0
- package/dist/functions/commands.d.ts +10 -10
- package/dist/functions/commands.js +87 -53
- package/dist/functions/manifest.d.ts +1 -0
- package/dist/functions/manifest.js +36 -0
- package/dist/functions/source-selection.d.ts +24 -0
- package/dist/functions/source-selection.js +67 -0
- package/dist/login.d.ts +8 -3
- package/dist/login.js +46 -34
- package/dist/main.d.ts +3 -1
- package/dist/main.js +41 -12
- package/dist/orm/delegate.js +25 -7
- package/dist/pat-hints.js +2 -2
- package/dist/pipeline/commands.d.ts +58 -0
- package/dist/pipeline/commands.js +330 -0
- package/dist/pipeline/lifecycle.d.ts +58 -0
- package/dist/pipeline/lifecycle.js +348 -0
- package/dist/pipeline/pinning.d.ts +5 -0
- package/dist/pipeline/pinning.js +9 -0
- package/dist/pipeline/templates.d.ts +11 -0
- package/dist/pipeline/templates.js +166 -0
- package/dist/process/build.d.ts +4 -0
- package/dist/process/build.js +33 -2
- package/dist/process/codegen.js +19 -1
- package/dist/process/commands.js +97 -47
- package/dist/process/compiler-subprocess.d.ts +29 -0
- package/dist/process/compiler-subprocess.js +99 -0
- package/dist/process/compiler-worker.d.ts +1 -0
- package/dist/process/compiler-worker.js +38 -0
- package/dist/process/lint.d.ts +8 -0
- package/dist/process/lint.js +84 -29
- package/dist/process/repo-install.js +18 -2
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +17 -12
- package/dist/secrets/commands.d.ts +1 -1
- package/dist/secrets/commands.js +18 -18
- 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,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.
|
|
11
|
-
*
|
|
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.
|
|
11
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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)
|