@sequenceholdings/artifact-studio 0.1.12 → 0.2.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/VERSION-PIN.md +54 -0
- package/dist/active-deploy-policy.d.ts +36 -0
- package/dist/active-deploy-policy.js +66 -0
- package/dist/api.js +59 -13
- package/dist/artifact-ignore.d.ts +16 -0
- package/dist/artifact-ignore.js +38 -0
- package/dist/atlas-ui-css-plugin.d.ts +15 -0
- package/dist/atlas-ui-css-plugin.js +69 -0
- package/dist/build-subprocess.d.ts +3 -1
- package/dist/build-subprocess.js +28 -6
- package/dist/build.d.ts +25 -0
- package/dist/build.js +97 -50
- package/dist/child-environment.d.ts +5 -0
- package/dist/child-environment.js +83 -0
- package/dist/cli.d.ts +40 -1
- package/dist/cli.js +173 -87
- package/dist/config.d.ts +0 -9
- package/dist/config.js +0 -18
- package/dist/deployment-validation.d.ts +9 -0
- package/dist/deployment-validation.js +70 -0
- package/dist/git-clone.js +16 -10
- package/dist/peer-versions.d.ts +19 -0
- package/dist/peer-versions.js +44 -0
- package/dist/prepare-build.d.ts +2 -0
- package/dist/prepare-build.js +27 -7
- package/dist/project.d.ts +3 -0
- package/dist/project.js +22 -6
- package/dist/runtime-chunks.d.ts +4 -0
- package/dist/runtime-chunks.js +15 -0
- package/dist/sdk.d.ts +10 -0
- package/dist/source-resolver.d.ts +8 -5
- package/dist/source-resolver.js +81 -32
- package/dist/templates/react-vite/AGENTS.md +90 -0
- package/dist/templates/react-vite/CLAUDE.md +4 -63
- package/dist/templates/react-vite/src/main.tsx +7 -1
- package/dist/templates/react-vite/src/styles.css +11 -0
- package/package.json +18 -5
- package/templates/react-vite/AGENTS.md +90 -0
- package/templates/react-vite/CLAUDE.md +4 -63
- package/templates/react-vite/src/main.tsx +7 -1
- package/templates/react-vite/src/styles.css +11 -0
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +0 -129
package/dist/cli.js
CHANGED
|
@@ -3,21 +3,22 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { getJson, getJsonOr404, postJson } from './api.js';
|
|
6
|
-
import { ENV_URLS, readLocalConfig, resolveEnvironment, writeLocalConfig,
|
|
7
|
-
import {
|
|
6
|
+
import { ENV_URLS, readLocalConfig, resolveEnvironment, writeLocalConfig, } from './config.js';
|
|
7
|
+
import { writeBuildArtifact } from './build.js';
|
|
8
8
|
import { buildArtifactStudioProjectIsolated } from './build-subprocess.js';
|
|
9
|
+
import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
|
|
9
10
|
import { prepareArtifactBuildRoot } from './prepare-build.js';
|
|
10
11
|
import { parseArtifactStudioManifest } from './manifest.js';
|
|
11
12
|
import { hasArtifactStudioManifest, readArtifactStudioSource } from './project.js';
|
|
12
13
|
import { localGitMetadata, parseSourceSpec, resolveArtifactSource, } from './source-resolver.js';
|
|
13
|
-
import { getAccessToken, loginWithPkce } from './auth.js';
|
|
14
14
|
import { acquireDevLock } from './dev-lock.js';
|
|
15
15
|
import { runWatchLoop } from './watch-loop.js';
|
|
16
16
|
import { hashLinkedSources, parseLinks, rebuildLinkedDep, resolveLinkedDeps, syncLinkedDist, } from './dev-link.js';
|
|
17
17
|
import { lintArtifactSandbox, reportSandboxWarnings } from './sandbox-lint.js';
|
|
18
|
+
import { assertActiveDeployProvenance } from './active-deploy-policy.js';
|
|
18
19
|
import yaml from 'js-yaml';
|
|
19
20
|
const COMMANDS = [
|
|
20
|
-
'init', '
|
|
21
|
+
'init', 'whoami', 'link', 'env', 'status', 'list', 'show',
|
|
21
22
|
'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
|
|
22
23
|
];
|
|
23
24
|
const USAGE = `usage:
|
|
@@ -38,18 +39,29 @@ const USAGE = `usage:
|
|
|
38
39
|
seq-studio artifact rollback <deployment-id> --env <env>
|
|
39
40
|
|
|
40
41
|
Source: a local [dir] (default), a platform git-service repo (--repo <ns>/<name>),
|
|
41
|
-
or
|
|
42
|
+
or a public HTTPS git URL (--git-url <url>). --ref picks a branch/tag/commit (default: the
|
|
42
43
|
repo's default branch). The target project comes from the source's
|
|
43
44
|
artifact.bundle.yml project_id.
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
Official (active) deploys require a clean \`main\` checkout with a resolved
|
|
47
|
+
git commit — merge first, then \`deploy --repo …\` (or a clean local \`main\`
|
|
48
|
+
checkout). Feature branches and dirty trees are preview-only
|
|
49
|
+
(\`artifact dev\` / preview channel).
|
|
50
|
+
|
|
51
|
+
Interactive --repo builds clone over smart-HTTP and require a repo:read git
|
|
52
|
+
PAT in ATLAS_GIT_PAT — create one with \`seq-studio auth pat create --scopes
|
|
53
|
+
repo:read\` or in Atlas → Settings → Tokens. Headless M2M builds use JSON
|
|
54
|
+
materialize and accept only platform-managed --repo sources. (--env +
|
|
55
|
+
seq-studio login are still needed to resolve the repo and upload the
|
|
56
|
+
deployment.)
|
|
50
57
|
|
|
51
58
|
Authenticate with: seq-studio login`;
|
|
52
59
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
60
|
+
if (argv.some((arg) => arg === '--token' || arg.startsWith('--token='))) {
|
|
61
|
+
console.error('Artifact Studio does not accept --token bearer injection. ' +
|
|
62
|
+
'Authenticate with seq-studio login.');
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
53
65
|
const parsed = parseArgs(argv);
|
|
54
66
|
if ('error' in parsed) {
|
|
55
67
|
console.error(parsed.error);
|
|
@@ -57,8 +69,6 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
57
69
|
}
|
|
58
70
|
switch (parsed.command) {
|
|
59
71
|
case 'init': return initCommand(parsed);
|
|
60
|
-
case 'login': return loginCommand(parsed);
|
|
61
|
-
case 'logout': return logoutCommand();
|
|
62
72
|
case 'whoami': return whoamiCommand(parsed);
|
|
63
73
|
case 'link': return linkCommand(parsed);
|
|
64
74
|
case 'env': return envCommand(parsed);
|
|
@@ -119,22 +129,6 @@ async function initCommand(args) {
|
|
|
119
129
|
console.log('Next: seq-studio login && seq-studio artifact dev --env <env> (see: seq-studio envs list)');
|
|
120
130
|
return 0;
|
|
121
131
|
}
|
|
122
|
-
async function loginCommand(args) {
|
|
123
|
-
const token = typeof args.flags.token === 'string' ? args.flags.token : undefined;
|
|
124
|
-
if (token) {
|
|
125
|
-
await writeTokenConfig({ accessToken: token });
|
|
126
|
-
console.log('[seq-studio] saved access token');
|
|
127
|
-
return 0;
|
|
128
|
-
}
|
|
129
|
-
await loginWithPkce();
|
|
130
|
-
console.log('[seq-studio] logged in');
|
|
131
|
-
return 0;
|
|
132
|
-
}
|
|
133
|
-
async function logoutCommand() {
|
|
134
|
-
await writeTokenConfig({});
|
|
135
|
-
console.log('[seq-studio] logged out');
|
|
136
|
-
return 0;
|
|
137
|
-
}
|
|
138
132
|
async function whoamiCommand(args) {
|
|
139
133
|
const env = await envFromArgs(args);
|
|
140
134
|
const token = await getRequiredToken(args);
|
|
@@ -315,25 +309,31 @@ async function showCommand(args) {
|
|
|
315
309
|
const deployment = project.activeDeployment;
|
|
316
310
|
if (deployment) {
|
|
317
311
|
console.log(` active deployment:`);
|
|
318
|
-
|
|
319
|
-
console.log(` sourceHash: ${deployment.sourceHash}`);
|
|
320
|
-
if (deployment.gitCommit) {
|
|
321
|
-
const dirty = deployment.gitDirty ? ' (dirty)' : '';
|
|
322
|
-
const branch = deployment.gitBranch ? ` on ${deployment.gitBranch}` : '';
|
|
323
|
-
console.log(` git: ${deployment.gitCommit.slice(0, 12)}${branch}${dirty}`);
|
|
324
|
-
}
|
|
325
|
-
if (deployment.deployMessage)
|
|
326
|
-
console.log(` message: ${deployment.deployMessage}`);
|
|
327
|
-
if (deployment.createdAt)
|
|
328
|
-
console.log(` deployedAt: ${deployment.createdAt}`);
|
|
329
|
-
if (deployment.createdBy)
|
|
330
|
-
console.log(` deployedBy: ${deployment.createdBy}`);
|
|
312
|
+
logActiveDeploymentDetails(deployment);
|
|
331
313
|
}
|
|
332
314
|
else {
|
|
333
315
|
console.log(` active deployment: none`);
|
|
334
316
|
}
|
|
335
317
|
return 0;
|
|
336
318
|
}
|
|
319
|
+
function logActiveDeploymentDetails(deployment) {
|
|
320
|
+
console.log(` version: ${deployment.version}`);
|
|
321
|
+
console.log(` sourceHash: ${deployment.sourceHash}`);
|
|
322
|
+
if (deployment.gitCommit) {
|
|
323
|
+
const dirty = deployment.gitDirty ? ' (dirty)' : '';
|
|
324
|
+
const branch = deployment.gitBranch ? ` on ${deployment.gitBranch}` : '';
|
|
325
|
+
console.log(` git: ${deployment.gitCommit.slice(0, 12)}${branch}${dirty}`);
|
|
326
|
+
}
|
|
327
|
+
if (deployment.cliVersion || deployment.atlasUiVersion) {
|
|
328
|
+
console.log(` peers: artifact-studio@${deployment.cliVersion ?? '?'} atlas-ui@${deployment.atlasUiVersion ?? '?'}`);
|
|
329
|
+
}
|
|
330
|
+
if (deployment.deployMessage)
|
|
331
|
+
console.log(` message: ${deployment.deployMessage}`);
|
|
332
|
+
if (deployment.createdAt)
|
|
333
|
+
console.log(` deployedAt: ${deployment.createdAt}`);
|
|
334
|
+
if (deployment.createdBy)
|
|
335
|
+
console.log(` deployedBy: ${deployment.createdBy}`);
|
|
336
|
+
}
|
|
337
337
|
async function validateCommand(args) {
|
|
338
338
|
const dir = resolve(args.positional[0] ?? '.');
|
|
339
339
|
const source = await readArtifactStudioSource(dir);
|
|
@@ -345,15 +345,23 @@ async function validateCommand(args) {
|
|
|
345
345
|
}
|
|
346
346
|
async function buildPreparedArtifactSource(source) {
|
|
347
347
|
await prepareArtifactBuildRoot(source.dir, { remote: source.remote });
|
|
348
|
-
return
|
|
348
|
+
return buildArtifactStudioProjectIsolated(source.dir);
|
|
349
349
|
}
|
|
350
350
|
async function buildCommand(args) {
|
|
351
351
|
const spec = parseSourceSpec(args);
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
352
|
+
// Local builds stay offline. Public git URLs need no bearer token, but an
|
|
353
|
+
// optional auth lookup still identifies a selected M2M principal so the
|
|
354
|
+
// source policy can reject arbitrary CI input before cloning.
|
|
355
|
+
const auth = spec.kind === 'git-service'
|
|
356
|
+
? await getRequiredAuth(args)
|
|
357
|
+
: spec.kind === 'git-url'
|
|
358
|
+
? await getOptionalAuth(args)
|
|
359
|
+
: null;
|
|
360
|
+
const remote = spec.kind === 'git-service' && auth
|
|
361
|
+
? { authMode: auth.authMode, baseUrl: (await envFromArgs(args)).url, token: auth.token }
|
|
362
|
+
: auth
|
|
363
|
+
? { authMode: auth.authMode }
|
|
364
|
+
: {};
|
|
357
365
|
const source = await resolveArtifactSource(spec, remote);
|
|
358
366
|
try {
|
|
359
367
|
const result = await buildPreparedArtifactSource(source);
|
|
@@ -364,7 +372,8 @@ async function buildCommand(args) {
|
|
|
364
372
|
await writeBuildArtifact(result, out);
|
|
365
373
|
console.log(`[seq-studio] built ${result.manifest.bundle.name}`);
|
|
366
374
|
console.log(`[seq-studio] source: ${result.sourceHash}`);
|
|
367
|
-
console.log(`[seq-studio] bundle: ${result.bundleHash}`);
|
|
375
|
+
console.log(`[seq-studio] bundle: ${result.bundleHash} (${result.bundleFormat}, ${result.chunks.length} chunks)`);
|
|
376
|
+
console.log(`[seq-studio] peers: artifact-studio@${result.peerVersions.cliVersion} atlas-ui@${result.peerVersions.atlasUiVersion}`);
|
|
368
377
|
console.log(`[seq-studio] wrote ${out}`);
|
|
369
378
|
reportSandboxWarnings(lintArtifactSandbox(result.files));
|
|
370
379
|
return 0;
|
|
@@ -375,9 +384,14 @@ async function buildCommand(args) {
|
|
|
375
384
|
}
|
|
376
385
|
async function planCommand(args) {
|
|
377
386
|
const env = await envFromArgs(args);
|
|
378
|
-
const
|
|
387
|
+
const auth = await getOptionalAuth(args);
|
|
388
|
+
const token = auth?.token ?? null;
|
|
379
389
|
const spec = parseSourceSpec(args);
|
|
380
|
-
const source = await resolveArtifactSource(spec, {
|
|
390
|
+
const source = await resolveArtifactSource(spec, {
|
|
391
|
+
...(auth ? { authMode: auth.authMode } : {}),
|
|
392
|
+
baseUrl: env.url,
|
|
393
|
+
token,
|
|
394
|
+
});
|
|
381
395
|
try {
|
|
382
396
|
const result = await buildPreparedArtifactSource(source);
|
|
383
397
|
console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
|
|
@@ -388,12 +402,29 @@ async function planCommand(args) {
|
|
|
388
402
|
return 0;
|
|
389
403
|
}
|
|
390
404
|
const remote = await fetchRemoteProject({ env: env.url, token, slug: result.manifest.artifact.project_id });
|
|
391
|
-
if (!remote)
|
|
405
|
+
if (!remote) {
|
|
392
406
|
console.log(' action: create project and deploy');
|
|
393
|
-
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
// Mirror the deploy --skip-unchanged decision exactly: identical source
|
|
410
|
+
// with a different (or unrecorded) CLI/atlas-ui pin still redeploys, so
|
|
411
|
+
// plan must not report no-op there.
|
|
412
|
+
const active = remote.activeDeployment;
|
|
413
|
+
if (activeDeploymentMatchesSkipPin({
|
|
414
|
+
active,
|
|
415
|
+
sourceHash: result.sourceHash,
|
|
416
|
+
peerVersions: result.peerVersions,
|
|
417
|
+
})) {
|
|
394
418
|
console.log(' action: no-op');
|
|
395
|
-
|
|
396
|
-
|
|
419
|
+
}
|
|
420
|
+
else if (active && active.sourceHash === result.sourceHash) {
|
|
421
|
+
console.log(` action: deploy over ${active.version} ` +
|
|
422
|
+
`(source unchanged; pin moves ${active.cliVersion ?? 'unrecorded'}/${active.atlasUiVersion ?? 'unrecorded'} ` +
|
|
423
|
+
`→ ${result.peerVersions.cliVersion}/${result.peerVersions.atlasUiVersion})`);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
console.log(` action: deploy over ${active?.version ?? 'no active deployment'}`);
|
|
427
|
+
}
|
|
397
428
|
return 0;
|
|
398
429
|
}
|
|
399
430
|
finally {
|
|
@@ -402,25 +433,41 @@ async function planCommand(args) {
|
|
|
402
433
|
}
|
|
403
434
|
async function deployCommand(args) {
|
|
404
435
|
const env = await envFromArgs(args);
|
|
405
|
-
const
|
|
436
|
+
const auth = await getRequiredAuth(args);
|
|
437
|
+
const { token } = auth;
|
|
406
438
|
const spec = parseSourceSpec(args);
|
|
407
439
|
const explicitProjectId = typeof args.flags.project === 'string' ? args.flags.project : null;
|
|
408
|
-
const source = await resolveArtifactSource(spec, {
|
|
440
|
+
const source = await resolveArtifactSource(spec, {
|
|
441
|
+
authMode: auth.authMode,
|
|
442
|
+
baseUrl: env.url,
|
|
443
|
+
token,
|
|
444
|
+
});
|
|
409
445
|
try {
|
|
446
|
+
// Fail closed before build / project create — active deploys must already
|
|
447
|
+
// look like a clean `main` checkout. Preview (`dev`) never hits this path.
|
|
448
|
+
assertActiveDeployProvenance(source.provenance);
|
|
410
449
|
// --skip-unchanged: hash the raw source (no build needed) and no-op when
|
|
411
|
-
// the remote active deployment already matches
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
450
|
+
// the remote active deployment already matches *and* was built with the
|
|
451
|
+
// same CLI/atlas-ui pin. Lets CI redeploy every artifact on every release
|
|
452
|
+
// without paying a vite build + upload for the unchanged ones. sourceHash
|
|
453
|
+
// covers manifest + all source files; peer versions cover the force-alias
|
|
454
|
+
// inputs (DES-254) so a newer CLI still rebuilds identical source.
|
|
415
455
|
if (args.flags['skip-unchanged'] === true) {
|
|
416
456
|
const unbuilt = await readArtifactStudioSource(source.dir);
|
|
417
457
|
const slug = unbuilt.manifest.artifact.project_id;
|
|
418
458
|
const remote = explicitProjectId
|
|
419
459
|
? (await getJsonOr404({ baseUrl: env.url, token, path: `/api/artifact-studio/projects/${explicitProjectId}` }))?.project ?? null
|
|
420
460
|
: await fetchRemoteProject({ env: env.url, token, slug });
|
|
421
|
-
|
|
461
|
+
const peerVersions = resolveArtifactStudioPeerVersions();
|
|
462
|
+
const active = remote?.activeDeployment;
|
|
463
|
+
if (activeDeploymentMatchesSkipPin({
|
|
464
|
+
active,
|
|
465
|
+
sourceHash: unbuilt.sourceHash,
|
|
466
|
+
peerVersions,
|
|
467
|
+
}) && active) {
|
|
422
468
|
console.log(`[seq-studio] ${slug} unchanged on ${env.name} ` +
|
|
423
|
-
`(active ${
|
|
469
|
+
`(active ${active.version} matches source ${unbuilt.sourceHash.slice(0, 12)} ` +
|
|
470
|
+
`+ peers artifact-studio@${peerVersions.cliVersion} atlas-ui@${peerVersions.atlasUiVersion}) — skipping deploy`);
|
|
424
471
|
return 0;
|
|
425
472
|
}
|
|
426
473
|
}
|
|
@@ -436,6 +483,7 @@ async function deployCommand(args) {
|
|
|
436
483
|
linkConfigDir: spec.kind === 'local' ? source.dir : null,
|
|
437
484
|
allowCreate: args.flags['no-create'] !== true,
|
|
438
485
|
explicitProjectId,
|
|
486
|
+
envName: env.name,
|
|
439
487
|
});
|
|
440
488
|
const deployment = await uploadDeployment({ env: env.url, token, projectId: project.id, result, channel: 'active', provenance: source.provenance });
|
|
441
489
|
console.log(`[seq-studio] deployed ${project.title} ${deployment.version}`);
|
|
@@ -520,7 +568,9 @@ async function devCommand(args) {
|
|
|
520
568
|
// Resolve per push so the token refreshes across a long watch session.
|
|
521
569
|
const token = await getRequiredToken(args);
|
|
522
570
|
if (!projectId) {
|
|
523
|
-
|
|
571
|
+
// envName so a first-time `artifact dev` creation honors the manifest
|
|
572
|
+
// visibility too (and notes drift, once, when the project pre-exists).
|
|
573
|
+
const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir, envName: env.name });
|
|
524
574
|
projectId = project.id;
|
|
525
575
|
}
|
|
526
576
|
const deployment = await uploadDeployment({
|
|
@@ -684,7 +734,36 @@ async function rollbackCommand(args) {
|
|
|
684
734
|
console.log(`[seq-studio] rolled back to ${deploymentId}`);
|
|
685
735
|
return 0;
|
|
686
736
|
}
|
|
687
|
-
|
|
737
|
+
/**
|
|
738
|
+
* Whether `--skip-unchanged` may no-op: same source *and* same CLI/atlas-ui
|
|
739
|
+
* pin. Missing peer fields on older rows force a rebuild so the pin becomes
|
|
740
|
+
* observable (DES-254).
|
|
741
|
+
*/
|
|
742
|
+
export function activeDeploymentMatchesSkipPin({ active, sourceHash, peerVersions, }) {
|
|
743
|
+
if (!active)
|
|
744
|
+
return false;
|
|
745
|
+
if (active.sourceHash !== sourceHash)
|
|
746
|
+
return false;
|
|
747
|
+
if (!active.cliVersion || !active.atlasUiVersion)
|
|
748
|
+
return false;
|
|
749
|
+
return (active.cliVersion === peerVersions.cliVersion &&
|
|
750
|
+
active.atlasUiVersion === peerVersions.atlasUiVersion);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Deploys never change a live project's visibility — the Studio sharing UI /
|
|
754
|
+
* PATCH own that. The manifest's targets.<env>.visibility is honored at
|
|
755
|
+
* project CREATION only; on drift, say so instead of silently ignoring the
|
|
756
|
+
* field (which is how it read as decorative for a year).
|
|
757
|
+
*/
|
|
758
|
+
function warnOnVisibilityDrift({ project, declared, envName }) {
|
|
759
|
+
if (!declared || !envName || !project.visibility || declared === project.visibility)
|
|
760
|
+
return;
|
|
761
|
+
console.warn(`[seq-studio] note: artifact.bundle.yml targets.${envName}.visibility is "${declared}" but the project is ` +
|
|
762
|
+
`"${project.visibility}" on this environment — deploys never change visibility. Update it in Studio sharing ` +
|
|
763
|
+
`(or PATCH /api/artifact-studio/projects/${project.id}) if the manifest is what you intend.`);
|
|
764
|
+
}
|
|
765
|
+
async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null, envName = null }) {
|
|
766
|
+
const declaredVisibility = envName ? result.manifest.targets?.[envName]?.visibility : undefined;
|
|
688
767
|
if (explicitProjectId) {
|
|
689
768
|
const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${explicitProjectId}` });
|
|
690
769
|
if (!data)
|
|
@@ -693,19 +772,23 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
|
|
|
693
772
|
throw new Error(`--project ${explicitProjectId} resolves to slug "${data.project.slug}" but this source's ` +
|
|
694
773
|
`artifact.bundle.yml declares project_id "${result.manifest.artifact.project_id}" — refusing to deploy over a different project`);
|
|
695
774
|
}
|
|
775
|
+
warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
|
|
696
776
|
return data.project;
|
|
697
777
|
}
|
|
698
778
|
const config = linkConfigDir ? await readLocalConfig(linkConfigDir) : {};
|
|
699
779
|
if (config.projectId) {
|
|
700
780
|
const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
|
|
701
|
-
if (data)
|
|
781
|
+
if (data) {
|
|
782
|
+
warnOnVisibilityDrift({ project: data.project, declared: declaredVisibility, envName });
|
|
702
783
|
return data.project;
|
|
784
|
+
}
|
|
703
785
|
console.warn(`[seq-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
|
|
704
786
|
}
|
|
705
787
|
const existing = await fetchRemoteProject({ env, token, slug: result.manifest.artifact.project_id });
|
|
706
788
|
if (existing) {
|
|
707
789
|
if (linkConfigDir)
|
|
708
790
|
await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, linkConfigDir);
|
|
791
|
+
warnOnVisibilityDrift({ project: existing, declared: declaredVisibility, envName });
|
|
709
792
|
return existing;
|
|
710
793
|
}
|
|
711
794
|
if (!allowCreate) {
|
|
@@ -721,6 +804,7 @@ async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCre
|
|
|
721
804
|
title: result.manifest.artifact.title,
|
|
722
805
|
slug: result.manifest.artifact.project_id,
|
|
723
806
|
description: result.manifest.artifact.description ?? null,
|
|
807
|
+
...(declaredVisibility ? { visibility: declaredVisibility } : {}),
|
|
724
808
|
},
|
|
725
809
|
});
|
|
726
810
|
if (linkConfigDir)
|
|
@@ -746,11 +830,15 @@ async function uploadDeployment({ env, token, projectId, result, channel, previe
|
|
|
746
830
|
manifest: result.manifest,
|
|
747
831
|
files: result.files,
|
|
748
832
|
bundle: result.bundle,
|
|
833
|
+
chunks: result.chunks.map((chunk) => ({ fileName: chunk.fileName, content: chunk.code })),
|
|
834
|
+
bundleFormat: result.bundleFormat,
|
|
749
835
|
sourceMap: result.sourceMap,
|
|
750
836
|
...(version ? { version } : {}),
|
|
751
837
|
channel,
|
|
752
838
|
previewKey,
|
|
753
839
|
deployMessage: message ?? null,
|
|
840
|
+
cliVersion: result.peerVersions.cliVersion,
|
|
841
|
+
atlasUiVersion: result.peerVersions.atlasUiVersion,
|
|
754
842
|
...provenance,
|
|
755
843
|
},
|
|
756
844
|
});
|
|
@@ -771,38 +859,36 @@ async function fetchRemoteProject({ env, token, slug, disambiguator }) {
|
|
|
771
859
|
}
|
|
772
860
|
return matches[0] ?? null;
|
|
773
861
|
}
|
|
774
|
-
async function
|
|
862
|
+
export async function getConfiguredDefaultEnv() {
|
|
775
863
|
const config = await readLocalConfig().catch(() => ({ defaultEnv: undefined }));
|
|
776
|
-
return
|
|
864
|
+
return config.defaultEnv;
|
|
865
|
+
}
|
|
866
|
+
async function envFromArgs(args) {
|
|
867
|
+
return resolveEnvironment(typeof args.flags.env === 'string' ? args.flags.env : undefined, await getConfiguredDefaultEnv());
|
|
777
868
|
}
|
|
778
|
-
/**
|
|
779
|
-
* Optional refreshing token provider, injected by an embedder (e.g. seq-studio)
|
|
780
|
-
* that owns the shared seqapi token file and can mint a fresh access token on
|
|
781
|
-
* demand. Consulted before the static ARTIFACT_STUDIO_TOKEN env var so a
|
|
782
|
-
* long-running `dev` watcher keeps deploying past the initial token's TTL —
|
|
783
|
-
* the env var is captured once at process start and never refreshes.
|
|
784
|
-
*/
|
|
785
869
|
let tokenProvider = null;
|
|
786
870
|
export function setTokenProvider(provider) {
|
|
787
871
|
tokenProvider = provider;
|
|
788
872
|
}
|
|
789
|
-
async function getRequiredToken(
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
873
|
+
async function getRequiredToken(_args) {
|
|
874
|
+
return (await getRequiredAuth(_args)).token;
|
|
875
|
+
}
|
|
876
|
+
async function getRequiredAuth(_args) {
|
|
877
|
+
const auth = await resolveAuth({ allowInteractiveLogin: true });
|
|
878
|
+
if (!auth)
|
|
879
|
+
throw new Error('Missing token. Authenticate with seq-studio login.');
|
|
880
|
+
return auth;
|
|
881
|
+
}
|
|
882
|
+
async function getOptionalAuth(_args) {
|
|
883
|
+
return resolveAuth({ allowInteractiveLogin: false });
|
|
794
884
|
}
|
|
795
|
-
async function
|
|
796
|
-
if (typeof args.flags.token === 'string')
|
|
797
|
-
return args.flags.token;
|
|
885
|
+
async function resolveAuth(options) {
|
|
798
886
|
if (tokenProvider) {
|
|
799
|
-
const provided = await tokenProvider();
|
|
887
|
+
const provided = await tokenProvider(options);
|
|
800
888
|
if (provided)
|
|
801
889
|
return provided;
|
|
802
890
|
}
|
|
803
|
-
|
|
804
|
-
return process.env.ARTIFACT_STUDIO_TOKEN;
|
|
805
|
-
return getAccessToken();
|
|
891
|
+
return null;
|
|
806
892
|
}
|
|
807
893
|
async function replaceInFile(path, pattern, value) {
|
|
808
894
|
const content = await import('node:fs/promises').then((fs) => fs.readFile(path, 'utf8'));
|
|
@@ -831,4 +917,4 @@ function warnIfInlinedManifestDriftsFromBundle(result) {
|
|
|
831
917
|
// This module is a pure library export. `seq-studio artifact` imports
|
|
832
918
|
// `runCli` from `@sequenceholdings/artifact-studio/cli` (see
|
|
833
919
|
// shared/services/studio-cli/README.md) and invokes it after injecting a
|
|
834
|
-
// shared base URL +
|
|
920
|
+
// shared base URL + internal token provider.
|
package/dist/config.d.ts
CHANGED
|
@@ -11,19 +11,10 @@ export interface LocalProjectConfig {
|
|
|
11
11
|
defaultEnv?: string;
|
|
12
12
|
previewKey?: string;
|
|
13
13
|
}
|
|
14
|
-
export interface TokenConfig {
|
|
15
|
-
accessToken?: string;
|
|
16
|
-
refreshToken?: string;
|
|
17
|
-
expiresAt?: number;
|
|
18
|
-
}
|
|
19
|
-
export declare function globalConfigDir(): string;
|
|
20
|
-
export declare function tokenConfigPath(): string;
|
|
21
14
|
export declare function localConfigPath(cwd?: string): string;
|
|
22
15
|
export declare function devLockPath(cwd?: string): string;
|
|
23
16
|
export declare function readLocalConfig(cwd?: string): Promise<LocalProjectConfig>;
|
|
24
17
|
export declare function writeLocalConfig(config: LocalProjectConfig, cwd?: string): Promise<void>;
|
|
25
|
-
export declare function readTokenConfig(): Promise<TokenConfig>;
|
|
26
|
-
export declare function writeTokenConfig(config: TokenConfig): Promise<void>;
|
|
27
18
|
export declare function knownEnvUrls(): Record<string, string>;
|
|
28
19
|
export declare function resolveEnvironment(name: string | undefined, fallback?: string): {
|
|
29
20
|
name: string;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { homedir } from 'node:os';
|
|
5
4
|
/**
|
|
6
5
|
* Only `local` ships in the published package. Deployed-environment URLs
|
|
7
6
|
* are resolved by the embedding CLI (seq-studio), which discovers the
|
|
@@ -11,12 +10,6 @@ import { homedir } from 'node:os';
|
|
|
11
10
|
export const ENV_URLS = {
|
|
12
11
|
local: 'http://localhost:5001',
|
|
13
12
|
};
|
|
14
|
-
export function globalConfigDir() {
|
|
15
|
-
return join(homedir(), '.config', 'sequence-artifact-studio');
|
|
16
|
-
}
|
|
17
|
-
export function tokenConfigPath() {
|
|
18
|
-
return join(globalConfigDir(), 'tokens.json');
|
|
19
|
-
}
|
|
20
13
|
export function localConfigPath(cwd = process.cwd()) {
|
|
21
14
|
return join(resolve(cwd), '.artifact-studio', 'config.json');
|
|
22
15
|
}
|
|
@@ -34,17 +27,6 @@ export async function writeLocalConfig(config, cwd = process.cwd()) {
|
|
|
34
27
|
await mkdir(dirname(path), { recursive: true });
|
|
35
28
|
await writeFile(path, JSON.stringify(config, null, 2) + '\n', 'utf8');
|
|
36
29
|
}
|
|
37
|
-
export async function readTokenConfig() {
|
|
38
|
-
const path = tokenConfigPath();
|
|
39
|
-
if (!existsSync(path))
|
|
40
|
-
return {};
|
|
41
|
-
return JSON.parse(await readFile(path, 'utf8'));
|
|
42
|
-
}
|
|
43
|
-
export async function writeTokenConfig(config) {
|
|
44
|
-
const path = tokenConfigPath();
|
|
45
|
-
await mkdir(dirname(path), { recursive: true });
|
|
46
|
-
await writeFile(path, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
47
|
-
}
|
|
48
30
|
/**
|
|
49
31
|
* Environment map injected by the embedding CLI (seq-studio sets
|
|
50
32
|
* ARTIFACT_STUDIO_ENV_URLS to the JSON name→url map of environments visible
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function validateDeploymentBaseUrl(input: string): string;
|
|
2
|
+
export declare function validateDeploymentAudience({ audience, baseUrl, }: {
|
|
3
|
+
audience: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
}): string;
|
|
6
|
+
export declare function authenticatedRequestUrl({ baseUrl, path, }: {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
path: string;
|
|
9
|
+
}): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const TRUSTED_DEPLOYMENT_DOMAIN = 'seqholdings.com';
|
|
2
|
+
export function validateDeploymentBaseUrl(input) {
|
|
3
|
+
if (input !== input.trim()) {
|
|
4
|
+
throw new Error(`Invalid deployment URL '${input}'.`);
|
|
5
|
+
}
|
|
6
|
+
let parsed;
|
|
7
|
+
try {
|
|
8
|
+
parsed = new URL(input);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
throw new Error(`Invalid deployment URL '${input}'.`);
|
|
12
|
+
}
|
|
13
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
14
|
+
const originOnly = !parsed.username &&
|
|
15
|
+
!parsed.password &&
|
|
16
|
+
(parsed.pathname === '' || parsed.pathname === '/') &&
|
|
17
|
+
!parsed.search &&
|
|
18
|
+
!parsed.hash;
|
|
19
|
+
const trustedHttps = parsed.protocol === 'https:' &&
|
|
20
|
+
(hostname === TRUSTED_DEPLOYMENT_DOMAIN ||
|
|
21
|
+
hostname.endsWith(`.${TRUSTED_DEPLOYMENT_DOMAIN}`)) &&
|
|
22
|
+
parsed.port === '';
|
|
23
|
+
const localHttp = parsed.protocol === 'http:' &&
|
|
24
|
+
['localhost', '127.0.0.1', '[::1]'].includes(hostname);
|
|
25
|
+
if (!originOnly || (!trustedHttps && !localHttp)) {
|
|
26
|
+
throw new Error(`Untrusted deployment URL '${input}': expected an HTTPS origin under ` +
|
|
27
|
+
`*.${TRUSTED_DEPLOYMENT_DOMAIN}, or an HTTP localhost/loopback origin.`);
|
|
28
|
+
}
|
|
29
|
+
return trustedHttps ? `https://${hostname}` : parsed.origin.toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
export function validateDeploymentAudience({ audience, baseUrl, }) {
|
|
32
|
+
const expected = `${baseUrl}/api`;
|
|
33
|
+
if (audience !== expected) {
|
|
34
|
+
throw new Error(`Untrusted Auth0 audience '${audience}': deployment ${baseUrl} must use '${expected}'.`);
|
|
35
|
+
}
|
|
36
|
+
return audience;
|
|
37
|
+
}
|
|
38
|
+
export function authenticatedRequestUrl({ baseUrl, path, }) {
|
|
39
|
+
const trustedOrigin = validateDeploymentBaseUrl(baseUrl);
|
|
40
|
+
if (!path.startsWith('/')) {
|
|
41
|
+
throw new Error(`Invalid authenticated request path '${path}': expected an absolute path.`);
|
|
42
|
+
}
|
|
43
|
+
if (path.startsWith('//')) {
|
|
44
|
+
throw new Error(`Invalid authenticated request path '${path}': scheme-relative paths are not allowed.`);
|
|
45
|
+
}
|
|
46
|
+
if ([...path].some((character) => character.charCodeAt(0) < 0x20)) {
|
|
47
|
+
throw new Error(`Invalid authenticated request path '${path}': control character.`);
|
|
48
|
+
}
|
|
49
|
+
const separatorIndex = path.search(/[?#]/);
|
|
50
|
+
const rawPathname = separatorIndex === -1 ? path : path.slice(0, separatorIndex);
|
|
51
|
+
const traversalCandidate = rawPathname
|
|
52
|
+
.replace(/%2e/gi, '.')
|
|
53
|
+
.replace(/%2f/gi, '/')
|
|
54
|
+
.replace(/%5c/gi, '\\');
|
|
55
|
+
if (traversalCandidate.includes('\\') ||
|
|
56
|
+
traversalCandidate.split('/').some((segment) => segment === '.' || segment === '..')) {
|
|
57
|
+
throw new Error(`Invalid authenticated request path '${path}': traversal is not allowed.`);
|
|
58
|
+
}
|
|
59
|
+
let request;
|
|
60
|
+
try {
|
|
61
|
+
request = new URL(path, `${trustedOrigin}/`);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error(`Invalid authenticated request path '${path}'.`);
|
|
65
|
+
}
|
|
66
|
+
if (request.origin.toLowerCase() !== trustedOrigin) {
|
|
67
|
+
throw new Error(`Untrusted authenticated request origin '${request.origin}': expected '${trustedOrigin}'.`);
|
|
68
|
+
}
|
|
69
|
+
return request.href;
|
|
70
|
+
}
|
package/dist/git-clone.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* HTTPS clone URL plus the PAT separately.
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
+
import { createScrubbedChildEnv } from './child-environment.js';
|
|
16
17
|
/** Read the git PAT from the environment (`ATLAS_GIT_PAT`), trimmed. */
|
|
17
18
|
export function resolveGitPatFromEnv() {
|
|
18
19
|
const value = process.env.ATLAS_GIT_PAT?.trim();
|
|
@@ -108,6 +109,7 @@ async function spawnGit({ args, env, cwd, timeoutMs = GIT_CLONE_TIMEOUT_MS, }) {
|
|
|
108
109
|
*/
|
|
109
110
|
export async function withGitAskpass({ pat, fn, }) {
|
|
110
111
|
const dir = await mkdtemp(join(tmpdir(), 'artifact-askpass-'));
|
|
112
|
+
const homeDir = join(dir, 'home');
|
|
111
113
|
const scriptPath = join(dir, 'askpass.sh');
|
|
112
114
|
const script = [
|
|
113
115
|
'#!/bin/sh',
|
|
@@ -117,17 +119,21 @@ export async function withGitAskpass({ pat, fn, }) {
|
|
|
117
119
|
'esac',
|
|
118
120
|
'',
|
|
119
121
|
].join('\n');
|
|
122
|
+
await mkdir(homeDir, { recursive: true });
|
|
120
123
|
await writeFile(scriptPath, script, { mode: 0o700 });
|
|
121
124
|
try {
|
|
122
|
-
return await fn({
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
return await fn(createScrubbedChildEnv({
|
|
126
|
+
homeDir,
|
|
127
|
+
additionalEnv: {
|
|
128
|
+
GIT_ASKPASS: scriptPath,
|
|
129
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
130
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
131
|
+
SEQ_STUDIO_GIT_ASKPASS_PASSWORD: pat,
|
|
132
|
+
GIT_CONFIG_COUNT: '1',
|
|
133
|
+
GIT_CONFIG_KEY_0: 'credential.helper',
|
|
134
|
+
GIT_CONFIG_VALUE_0: '',
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
131
137
|
}
|
|
132
138
|
finally {
|
|
133
139
|
await rm(dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Versions of the packages that actually land in an artifact bundle under the
|
|
3
|
+
* CLI atlas-ui stamp (DES-254). See `VERSION-PIN.md`.
|
|
4
|
+
*
|
|
5
|
+
* - `cliVersion` — `@sequenceholdings/artifact-studio` (the build package that
|
|
6
|
+
* owns `PEER_RESOLVE_ALIAS`).
|
|
7
|
+
* - `atlasUiVersion` — the `@sequenceholdings/atlas-ui` copy resolved from
|
|
8
|
+
* *that* package's dependency tree (not the artifact's package.json).
|
|
9
|
+
*/
|
|
10
|
+
export interface ArtifactStudioPeerVersions {
|
|
11
|
+
cliVersion: string;
|
|
12
|
+
atlasUiVersion: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the building CLI + force-aliased atlas-ui versions from disk.
|
|
16
|
+
* Throws if either package.json is unreadable — a silent null here would
|
|
17
|
+
* undermine the observability DES-254 adds for the blast-radius scanner.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveArtifactStudioPeerVersions(): ArtifactStudioPeerVersions;
|