@sequenceholdings/artifact-studio 0.1.6 → 0.1.9
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/dist/api.js +66 -9
- package/dist/auth.js +1 -1
- package/dist/build-subprocess.d.ts +15 -0
- package/dist/build-subprocess.js +65 -0
- package/dist/build-worker.d.ts +1 -0
- package/dist/build-worker.js +28 -0
- package/dist/build.d.ts +11 -0
- package/dist/build.js +85 -14
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +486 -118
- package/dist/config.d.ts +1 -0
- package/dist/config.js +4 -1
- package/dist/dev-link.d.ts +60 -0
- package/dist/dev-link.js +155 -0
- package/dist/dev-lock.d.ts +5 -0
- package/dist/dev-lock.js +83 -0
- package/dist/git-clone.d.ts +41 -0
- package/dist/git-clone.js +178 -0
- package/dist/git-service-client.d.ts +32 -0
- package/dist/git-service-client.js +122 -0
- package/dist/hash.d.ts +12 -0
- package/dist/hash.js +13 -1
- package/dist/lockfile-origin.d.ts +6 -0
- package/dist/lockfile-origin.js +41 -0
- package/dist/manifest-custom-roles.d.ts +63 -0
- package/dist/manifest-custom-roles.js +194 -0
- package/dist/manifest.d.ts +31 -0
- package/dist/manifest.js +77 -0
- package/dist/prepare-build.d.ts +21 -0
- package/dist/prepare-build.js +94 -0
- package/dist/project.js +2 -1
- package/dist/sandbox-lint.d.ts +21 -0
- package/dist/sandbox-lint.js +95 -0
- package/dist/sanitize-remote-tree.d.ts +19 -0
- package/dist/sanitize-remote-tree.js +52 -0
- package/dist/sdk.d.ts +49 -0
- package/dist/source-resolver.d.ts +68 -0
- package/dist/source-resolver.js +202 -0
- package/dist/templates/react-vite/CLAUDE.md +18 -7
- package/dist/templates/react-vite/package.json +8 -7
- package/dist/trusted-install.d.ts +13 -0
- package/dist/trusted-install.js +186 -0
- package/dist/watch-loop.d.ts +26 -0
- package/dist/watch-loop.js +31 -0
- package/package.json +29 -9
- package/templates/react-vite/CLAUDE.md +18 -7
- package/templates/react-vite/package.json +8 -7
- package/dist/bin.d.ts +0 -2
- package/dist/bin.js +0 -8
package/dist/cli.js
CHANGED
|
@@ -1,33 +1,54 @@
|
|
|
1
1
|
import { cp, mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
4
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
4
|
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { getJson, getJsonOr404, postJson } from './api.js';
|
|
7
6
|
import { readLocalConfig, resolveEnvironment, writeLocalConfig, writeTokenConfig, } from './config.js';
|
|
8
7
|
import { buildArtifactStudioProject, writeBuildArtifact } from './build.js';
|
|
8
|
+
import { buildArtifactStudioProjectIsolated } from './build-subprocess.js';
|
|
9
|
+
import { prepareArtifactBuildRoot } from './prepare-build.js';
|
|
10
|
+
import { parseArtifactStudioManifest } from './manifest.js';
|
|
9
11
|
import { hasArtifactStudioManifest, readArtifactStudioSource } from './project.js';
|
|
12
|
+
import { localGitMetadata, parseSourceSpec, resolveArtifactSource, } from './source-resolver.js';
|
|
10
13
|
import { getAccessToken, loginWithPkce } from './auth.js';
|
|
14
|
+
import { acquireDevLock } from './dev-lock.js';
|
|
15
|
+
import { runWatchLoop } from './watch-loop.js';
|
|
16
|
+
import { hashLinkedSources, parseLinks, rebuildLinkedDep, resolveLinkedDeps, syncLinkedDist, } from './dev-link.js';
|
|
17
|
+
import { lintArtifactSandbox, reportSandboxWarnings } from './sandbox-lint.js';
|
|
18
|
+
import yaml from 'js-yaml';
|
|
11
19
|
const COMMANDS = [
|
|
12
|
-
'init', 'login', 'logout', 'whoami', 'link', 'env', 'status',
|
|
20
|
+
'init', 'login', 'logout', 'whoami', 'link', 'env', 'status', 'list', 'show',
|
|
13
21
|
'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
|
|
14
22
|
];
|
|
15
23
|
const USAGE = `usage:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
seq-studio artifact init <dir>
|
|
25
|
+
seq-studio artifact whoami --env <env>
|
|
26
|
+
seq-studio artifact link --env <env> [--project <id>]
|
|
27
|
+
seq-studio artifact env list|use <env>
|
|
28
|
+
seq-studio artifact status [--env <env>]
|
|
29
|
+
seq-studio artifact list --env <env>
|
|
30
|
+
seq-studio artifact show <slug-or-id> --env <env>
|
|
31
|
+
seq-studio artifact validate [dir]
|
|
32
|
+
seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--out dist/artifact-bundle.json]
|
|
33
|
+
seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] --env <env>
|
|
34
|
+
seq-studio artifact deploy [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] --env <env> [--skip-unchanged] [--no-create] [--project <id>]
|
|
35
|
+
seq-studio artifact dev [dir] --env <env> [--once] [--key default] [--link <pkgDir>[,<pkgDir>]]
|
|
36
|
+
seq-studio artifact promote <deployment-id> --env <env>
|
|
37
|
+
seq-studio artifact pull <project-id> --env <env> --out <dir>
|
|
38
|
+
seq-studio artifact rollback <deployment-id> --env <env>
|
|
39
|
+
|
|
40
|
+
Source: a local [dir] (default), a platform git-service repo (--repo <ns>/<name>),
|
|
41
|
+
or any git URL (--git-url <url>). --ref picks a branch/tag/commit (default: the
|
|
42
|
+
repo's default branch). The target project comes from the source's
|
|
43
|
+
artifact.bundle.yml project_id.
|
|
44
|
+
|
|
45
|
+
--repo clones over smart-HTTP and REQUIRES a git PAT in ATLAS_GIT_PAT
|
|
46
|
+
(repo:read scope) — create one with \`seq-studio auth pat create --scopes
|
|
47
|
+
repo:read\` or in Atlas → Settings → Tokens. It's the same token you clone the
|
|
48
|
+
repo with. (--env + seqapi login are still needed to resolve the repo and
|
|
49
|
+
upload the deployment.)
|
|
50
|
+
|
|
51
|
+
Authenticate with: seqapi login`;
|
|
31
52
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
32
53
|
const parsed = parseArgs(argv);
|
|
33
54
|
if ('error' in parsed) {
|
|
@@ -42,6 +63,8 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
42
63
|
case 'link': return linkCommand(parsed);
|
|
43
64
|
case 'env': return envCommand(parsed);
|
|
44
65
|
case 'status': return statusCommand(parsed);
|
|
66
|
+
case 'list': return listCommand(parsed);
|
|
67
|
+
case 'show': return showCommand(parsed);
|
|
45
68
|
case 'validate': return validateCommand(parsed);
|
|
46
69
|
case 'build': return buildCommand(parsed);
|
|
47
70
|
case 'plan': return planCommand(parsed);
|
|
@@ -92,24 +115,24 @@ async function initCommand(args) {
|
|
|
92
115
|
await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{slug\}\}/g, slug);
|
|
93
116
|
await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{title\}\}/g, titleize(slug));
|
|
94
117
|
await replaceInFile(join(dir, 'package.json'), /\{\{slug\}\}/g, slug);
|
|
95
|
-
console.log(`[
|
|
96
|
-
console.log('Next:
|
|
118
|
+
console.log(`[seq-studio] initialized ${dir}`);
|
|
119
|
+
console.log('Next: seqapi login && seq-studio artifact dev --env staging');
|
|
97
120
|
return 0;
|
|
98
121
|
}
|
|
99
122
|
async function loginCommand(args) {
|
|
100
123
|
const token = typeof args.flags.token === 'string' ? args.flags.token : undefined;
|
|
101
124
|
if (token) {
|
|
102
125
|
await writeTokenConfig({ accessToken: token });
|
|
103
|
-
console.log('[
|
|
126
|
+
console.log('[seq-studio] saved access token');
|
|
104
127
|
return 0;
|
|
105
128
|
}
|
|
106
129
|
await loginWithPkce();
|
|
107
|
-
console.log('[
|
|
130
|
+
console.log('[seq-studio] logged in');
|
|
108
131
|
return 0;
|
|
109
132
|
}
|
|
110
133
|
async function logoutCommand() {
|
|
111
134
|
await writeTokenConfig({});
|
|
112
|
-
console.log('[
|
|
135
|
+
console.log('[seq-studio] logged out');
|
|
113
136
|
return 0;
|
|
114
137
|
}
|
|
115
138
|
async function whoamiCommand(args) {
|
|
@@ -120,8 +143,24 @@ async function whoamiCommand(args) {
|
|
|
120
143
|
token,
|
|
121
144
|
path: '/api/auth/me',
|
|
122
145
|
}).catch(async () => {
|
|
123
|
-
const projects = await getJson({
|
|
124
|
-
|
|
146
|
+
const projects = await getJson({
|
|
147
|
+
baseUrl: env.url,
|
|
148
|
+
token,
|
|
149
|
+
path: '/api/artifact-studio/projects',
|
|
150
|
+
});
|
|
151
|
+
const slugs = projects.projects
|
|
152
|
+
.map((p) => p.slug)
|
|
153
|
+
.filter((slug) => typeof slug === 'string' && slug.length > 0);
|
|
154
|
+
const preview = slugs.slice(0, 5);
|
|
155
|
+
const more = slugs.length > preview.length
|
|
156
|
+
? ` (+${slugs.length - preview.length} more — run seq-studio artifact list -e ${env.name})`
|
|
157
|
+
: slugs.length === 0
|
|
158
|
+
? ` — run seq-studio artifact list -e ${env.name}`
|
|
159
|
+
: '';
|
|
160
|
+
const slugPart = preview.length > 0 ? `: ${preview.join(', ')}` : '';
|
|
161
|
+
return {
|
|
162
|
+
subject: `authenticated (${slugs.length} visible project(s)${slugPart}${more})`,
|
|
163
|
+
};
|
|
125
164
|
});
|
|
126
165
|
console.log('user' in data ? (data.user?.email ?? data.user?.sub ?? 'authenticated') : data.subject);
|
|
127
166
|
return 0;
|
|
@@ -159,7 +198,7 @@ async function linkCommand(args) {
|
|
|
159
198
|
defaultEnv: env.name,
|
|
160
199
|
previewKey: 'default',
|
|
161
200
|
}, dir);
|
|
162
|
-
console.log(`[
|
|
201
|
+
console.log(`[seq-studio] linked ${project.title} (${project.id})`);
|
|
163
202
|
return 0;
|
|
164
203
|
}
|
|
165
204
|
async function envCommand(args) {
|
|
@@ -172,10 +211,10 @@ async function envCommand(args) {
|
|
|
172
211
|
const env = resolveEnvironment(value);
|
|
173
212
|
const config = await readLocalConfig();
|
|
174
213
|
await writeLocalConfig({ ...config, defaultEnv: env.name });
|
|
175
|
-
console.log(`[
|
|
214
|
+
console.log(`[seq-studio] default env set to ${env.name}`);
|
|
176
215
|
return 0;
|
|
177
216
|
}
|
|
178
|
-
console.error('
|
|
217
|
+
console.error('seq-studio artifact env list|use <env>');
|
|
179
218
|
return 1;
|
|
180
219
|
}
|
|
181
220
|
async function statusCommand(args) {
|
|
@@ -194,75 +233,292 @@ async function statusCommand(args) {
|
|
|
194
233
|
}
|
|
195
234
|
return 0;
|
|
196
235
|
}
|
|
236
|
+
async function listCommand(args) {
|
|
237
|
+
const env = await envFromArgs(args);
|
|
238
|
+
const token = await getRequiredToken(args);
|
|
239
|
+
const data = await getJson({
|
|
240
|
+
baseUrl: env.url,
|
|
241
|
+
token,
|
|
242
|
+
path: '/api/artifact-studio/projects',
|
|
243
|
+
});
|
|
244
|
+
if (data.projects.length === 0) {
|
|
245
|
+
console.log(`[seq-studio] no artifact projects visible on ${env.name}`);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
const sorted = [...data.projects].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
249
|
+
console.log(`[seq-studio] ${sorted.length} project${sorted.length === 1 ? '' : 's'} on ${env.name}:`);
|
|
250
|
+
for (const project of sorted) {
|
|
251
|
+
// Rows can carry activeDeploymentId with the deployment detail nulled
|
|
252
|
+
// (the serializer masks it for reduced-access rows) — label those
|
|
253
|
+
// "deployed" rather than falsely claiming "not deployed".
|
|
254
|
+
const active = project.activeDeployment
|
|
255
|
+
? project.activeDeployment.version
|
|
256
|
+
: project.activeDeploymentId
|
|
257
|
+
? 'deployed'
|
|
258
|
+
: 'not deployed';
|
|
259
|
+
const visibility = project.visibility ? ` ${project.visibility}` : '';
|
|
260
|
+
console.log(` ${project.slug} id=${project.id} [${active}]${visibility} ${project.title}`);
|
|
261
|
+
}
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
265
|
+
async function showCommand(args) {
|
|
266
|
+
const ref = args.positional[0];
|
|
267
|
+
if (!ref) {
|
|
268
|
+
console.error('usage: seq-studio artifact show <slug-or-id> --env <env>');
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
const env = await envFromArgs(args);
|
|
272
|
+
const token = await getRequiredToken(args);
|
|
273
|
+
// A UUID targets the project directly; anything else resolves by slug (hard
|
|
274
|
+
// error on cross-owner duplicates, same rule as deploy's slug resolution).
|
|
275
|
+
let projectId = ref;
|
|
276
|
+
if (!UUID_RE.test(ref)) {
|
|
277
|
+
const bySlug = await fetchRemoteProject({
|
|
278
|
+
env: env.url,
|
|
279
|
+
token,
|
|
280
|
+
slug: ref,
|
|
281
|
+
disambiguator: 'pass the project id instead: `seq-studio artifact show <id> --env <env>`.',
|
|
282
|
+
});
|
|
283
|
+
if (!bySlug) {
|
|
284
|
+
console.error(`[seq-studio] no project with slug "${ref}" on ${env.name}`);
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
projectId = bySlug.id;
|
|
288
|
+
}
|
|
289
|
+
const data = await getJsonOr404({
|
|
290
|
+
baseUrl: env.url,
|
|
291
|
+
token,
|
|
292
|
+
path: `/api/artifact-studio/projects/${projectId}`,
|
|
293
|
+
});
|
|
294
|
+
if (!data) {
|
|
295
|
+
console.error(`[seq-studio] project ${projectId} not found on ${env.name}`);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
const project = data.project;
|
|
299
|
+
console.log(`[seq-studio] project on ${env.name}:`);
|
|
300
|
+
console.log(` id: ${project.id}`);
|
|
301
|
+
console.log(` slug: ${project.slug}`);
|
|
302
|
+
console.log(` title: ${project.title}`);
|
|
303
|
+
if (project.description)
|
|
304
|
+
console.log(` description: ${project.description}`);
|
|
305
|
+
if (project.visibility)
|
|
306
|
+
console.log(` visibility: ${project.visibility}`);
|
|
307
|
+
if (project.appEligible !== undefined)
|
|
308
|
+
console.log(` appEligible: ${project.appEligible ? 'yes' : 'no'}`);
|
|
309
|
+
if (project.createdBy)
|
|
310
|
+
console.log(` createdBy: ${project.createdBy}`);
|
|
311
|
+
if (project.updatedAt)
|
|
312
|
+
console.log(` updated: ${project.updatedAt}`);
|
|
313
|
+
const deployment = project.activeDeployment;
|
|
314
|
+
if (deployment) {
|
|
315
|
+
console.log(` active deployment:`);
|
|
316
|
+
console.log(` version: ${deployment.version}`);
|
|
317
|
+
console.log(` sourceHash: ${deployment.sourceHash}`);
|
|
318
|
+
if (deployment.gitCommit) {
|
|
319
|
+
const dirty = deployment.gitDirty ? ' (dirty)' : '';
|
|
320
|
+
const branch = deployment.gitBranch ? ` on ${deployment.gitBranch}` : '';
|
|
321
|
+
console.log(` git: ${deployment.gitCommit.slice(0, 12)}${branch}${dirty}`);
|
|
322
|
+
}
|
|
323
|
+
if (deployment.deployMessage)
|
|
324
|
+
console.log(` message: ${deployment.deployMessage}`);
|
|
325
|
+
if (deployment.createdAt)
|
|
326
|
+
console.log(` deployedAt: ${deployment.createdAt}`);
|
|
327
|
+
if (deployment.createdBy)
|
|
328
|
+
console.log(` deployedBy: ${deployment.createdBy}`);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.log(` active deployment: none`);
|
|
332
|
+
}
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
197
335
|
async function validateCommand(args) {
|
|
198
336
|
const dir = resolve(args.positional[0] ?? '.');
|
|
199
337
|
const source = await readArtifactStudioSource(dir);
|
|
200
|
-
console.log(`[
|
|
201
|
-
console.log(`[
|
|
202
|
-
console.log(`[
|
|
338
|
+
console.log(`[seq-studio] valid ${source.manifest.bundle.name}`);
|
|
339
|
+
console.log(`[seq-studio] files: ${source.files.length}`);
|
|
340
|
+
console.log(`[seq-studio] source: ${source.sourceHash}`);
|
|
341
|
+
reportSandboxWarnings(lintArtifactSandbox(source.files));
|
|
203
342
|
return 0;
|
|
204
343
|
}
|
|
344
|
+
async function buildPreparedArtifactSource(source) {
|
|
345
|
+
await prepareArtifactBuildRoot(source.dir, { remote: source.remote });
|
|
346
|
+
return buildArtifactStudioProject(source.dir);
|
|
347
|
+
}
|
|
205
348
|
async function buildCommand(args) {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
349
|
+
const spec = parseSourceSpec(args);
|
|
350
|
+
// build is offline for a local/git-url source; only a git-service source
|
|
351
|
+
// needs an env + token to fetch the tree.
|
|
352
|
+
const remote = spec.kind === 'git-service'
|
|
353
|
+
? { baseUrl: (await envFromArgs(args)).url, token: await getRequiredToken(args) }
|
|
354
|
+
: {};
|
|
355
|
+
const source = await resolveArtifactSource(spec, remote);
|
|
356
|
+
try {
|
|
357
|
+
const result = await buildPreparedArtifactSource(source);
|
|
358
|
+
// A remote source's dir is a temp dir we tear down below, so default the
|
|
359
|
+
// bundle output next to the cwd rather than into the throwaway tree.
|
|
360
|
+
const defaultOutDir = spec.kind === 'local' ? source.dir : process.cwd();
|
|
361
|
+
const out = typeof args.flags.out === 'string' ? resolve(args.flags.out) : resolve(defaultOutDir, 'dist', 'artifact-bundle.json');
|
|
362
|
+
await writeBuildArtifact(result, out);
|
|
363
|
+
console.log(`[seq-studio] built ${result.manifest.bundle.name}`);
|
|
364
|
+
console.log(`[seq-studio] source: ${result.sourceHash}`);
|
|
365
|
+
console.log(`[seq-studio] bundle: ${result.bundleHash}`);
|
|
366
|
+
console.log(`[seq-studio] wrote ${out}`);
|
|
367
|
+
reportSandboxWarnings(lintArtifactSandbox(result.files));
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
await source.cleanup();
|
|
372
|
+
}
|
|
215
373
|
}
|
|
216
374
|
async function planCommand(args) {
|
|
217
|
-
const dir = resolve(args.positional[0] ?? '.');
|
|
218
375
|
const env = await envFromArgs(args);
|
|
219
376
|
const token = await getOptionalToken(args);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
console.log(
|
|
377
|
+
const spec = parseSourceSpec(args);
|
|
378
|
+
const source = await resolveArtifactSource(spec, { baseUrl: env.url, token });
|
|
379
|
+
try {
|
|
380
|
+
const result = await buildPreparedArtifactSource(source);
|
|
381
|
+
console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
|
|
382
|
+
console.log(` source: ${result.sourceHash}`);
|
|
383
|
+
console.log(` bundle: ${result.bundleHash}`);
|
|
384
|
+
if (!token) {
|
|
385
|
+
console.log(' remote: skipped (run seqapi login)');
|
|
386
|
+
return 0;
|
|
387
|
+
}
|
|
388
|
+
const remote = await fetchRemoteProject({ env: env.url, token, slug: result.manifest.artifact.project_id });
|
|
389
|
+
if (!remote)
|
|
390
|
+
console.log(' action: create project and deploy');
|
|
391
|
+
else if (remote.activeDeployment?.sourceHash === result.sourceHash)
|
|
392
|
+
console.log(' action: no-op');
|
|
393
|
+
else
|
|
394
|
+
console.log(` action: deploy over ${remote.activeDeployment?.version ?? 'no active deployment'}`);
|
|
226
395
|
return 0;
|
|
227
396
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
else if (remote.activeDeployment?.sourceHash === result.sourceHash)
|
|
232
|
-
console.log(' action: no-op');
|
|
233
|
-
else
|
|
234
|
-
console.log(` action: deploy over ${remote.activeDeployment?.version ?? 'no active deployment'}`);
|
|
235
|
-
return 0;
|
|
397
|
+
finally {
|
|
398
|
+
await source.cleanup();
|
|
399
|
+
}
|
|
236
400
|
}
|
|
237
401
|
async function deployCommand(args) {
|
|
238
|
-
const dir = resolve(args.positional[0] ?? '.');
|
|
239
402
|
const env = await envFromArgs(args);
|
|
240
403
|
const token = await getRequiredToken(args);
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
404
|
+
const spec = parseSourceSpec(args);
|
|
405
|
+
const explicitProjectId = typeof args.flags.project === 'string' ? args.flags.project : null;
|
|
406
|
+
const source = await resolveArtifactSource(spec, { baseUrl: env.url, token });
|
|
407
|
+
try {
|
|
408
|
+
// --skip-unchanged: hash the raw source (no build needed) and no-op when
|
|
409
|
+
// the remote active deployment already matches. Lets CI redeploy every
|
|
410
|
+
// artifact on every release without paying a vite build + upload for the
|
|
411
|
+
// unchanged ones. sourceHash covers manifest + all source files, so any
|
|
412
|
+
// real change falls through to a full deploy.
|
|
413
|
+
if (args.flags['skip-unchanged'] === true) {
|
|
414
|
+
const unbuilt = await readArtifactStudioSource(source.dir);
|
|
415
|
+
const slug = unbuilt.manifest.artifact.project_id;
|
|
416
|
+
const remote = explicitProjectId
|
|
417
|
+
? (await getJsonOr404({ baseUrl: env.url, token, path: `/api/artifact-studio/projects/${explicitProjectId}` }))?.project ?? null
|
|
418
|
+
: await fetchRemoteProject({ env: env.url, token, slug });
|
|
419
|
+
if (remote?.activeDeployment?.sourceHash === unbuilt.sourceHash) {
|
|
420
|
+
console.log(`[seq-studio] ${slug} unchanged on ${env.name} ` +
|
|
421
|
+
`(active ${remote.activeDeployment.version} matches source ${unbuilt.sourceHash.slice(0, 12)}) — skipping deploy`);
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const result = await buildPreparedArtifactSource(source);
|
|
426
|
+
warnIfInlinedManifestDriftsFromBundle(result);
|
|
427
|
+
// Local sources read/write the project link in their working dir; remote
|
|
428
|
+
// sources resolve strictly by manifest project_id (never a stray config.json
|
|
429
|
+
// in the materialized tree), so pass null.
|
|
430
|
+
const project = await ensureRemoteProject({
|
|
431
|
+
env: env.url,
|
|
432
|
+
token,
|
|
433
|
+
result,
|
|
434
|
+
linkConfigDir: spec.kind === 'local' ? source.dir : null,
|
|
435
|
+
allowCreate: args.flags['no-create'] !== true,
|
|
436
|
+
explicitProjectId,
|
|
437
|
+
});
|
|
438
|
+
const deployment = await uploadDeployment({ env: env.url, token, projectId: project.id, result, channel: 'active', provenance: source.provenance });
|
|
439
|
+
console.log(`[seq-studio] deployed ${project.title} ${deployment.version}`);
|
|
440
|
+
console.log(`${env.url}/artifact-studio/projects/${project.id}/deployments/${deployment.id}`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
await source.cleanup();
|
|
445
|
+
}
|
|
247
446
|
}
|
|
248
447
|
async function devCommand(args) {
|
|
448
|
+
if (args.flags.repo || args.flags['git-url']) {
|
|
449
|
+
throw new Error('dev watches a local folder; --repo / --git-url are only for build, plan, and deploy.');
|
|
450
|
+
}
|
|
249
451
|
const dir = resolve(args.positional[0] ?? '.');
|
|
250
452
|
const env = await envFromArgs(args);
|
|
251
|
-
|
|
453
|
+
// Validate auth up front for a fast failure, but resolve a fresh token on
|
|
454
|
+
// every push (below) so a long-running watcher survives access-token expiry
|
|
455
|
+
// instead of dying with "Authentication failed" once the initial token's TTL
|
|
456
|
+
// elapses.
|
|
457
|
+
await getRequiredToken(args);
|
|
252
458
|
const config = await readLocalConfig(dir);
|
|
253
459
|
const previewKey = typeof args.flags.key === 'string' ? args.flags.key : config.previewKey ?? 'default';
|
|
254
460
|
const once = args.flags.once === true;
|
|
461
|
+
const controller = new AbortController();
|
|
255
462
|
let projectId = null;
|
|
256
463
|
let lastSourceHash = null;
|
|
464
|
+
let lastDepsHash = null;
|
|
257
465
|
let buildCount = 0;
|
|
466
|
+
// `--link` lets the watcher rebuild local source packages (e.g. atlas-ui) and
|
|
467
|
+
// refresh the artifact automatically, instead of the rebuild/reinstall/restart
|
|
468
|
+
// dance. Resolved once up front so a bad path fails fast.
|
|
469
|
+
const linkedDeps = resolveLinkedDeps({ links: parseLinks(args.flags.link) });
|
|
470
|
+
for (const dep of linkedDeps) {
|
|
471
|
+
const where = relative(process.cwd(), dep.dir) || '.';
|
|
472
|
+
console.log(`[seq-studio] linked ${dep.name} ← ${where}${dep.inPlace ? ' (in place)' : ''}`);
|
|
473
|
+
}
|
|
474
|
+
// Rebuild any linked package whose source changed and copy its dist into the
|
|
475
|
+
// location the artifact build resolves it from. Returns true when a rebuild
|
|
476
|
+
// happened so the caller forces an artifact rebuild even if its own source is
|
|
477
|
+
// unchanged (the artifact source hash doesn't cover dependency dist).
|
|
478
|
+
async function syncLinkedDeps() {
|
|
479
|
+
if (linkedDeps.length === 0)
|
|
480
|
+
return false;
|
|
481
|
+
const depsHash = await hashLinkedSources(linkedDeps);
|
|
482
|
+
if (depsHash === lastDepsHash)
|
|
483
|
+
return false;
|
|
484
|
+
for (const dep of linkedDeps) {
|
|
485
|
+
console.log(`[seq-studio] rebuilding ${dep.name}…`);
|
|
486
|
+
await rebuildLinkedDep(dep, controller.signal);
|
|
487
|
+
await syncLinkedDist(dep);
|
|
488
|
+
}
|
|
489
|
+
// Record the hash only after every linked dep rebuilt + synced successfully,
|
|
490
|
+
// so a failed rebuild leaves the hash stale and the next tick retries instead
|
|
491
|
+
// of going quiet until the source changes again.
|
|
492
|
+
lastDepsHash = depsHash;
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
258
495
|
async function pushPreview() {
|
|
259
|
-
|
|
260
|
-
|
|
496
|
+
// Rebuild linked source packages first; a dep change forces the artifact
|
|
497
|
+
// rebuild below even when the artifact's own source is untouched.
|
|
498
|
+
const depsChanged = await syncLinkedDeps();
|
|
499
|
+
// Cheap in-process change detection (fs read + hash, no Vite) so we only
|
|
500
|
+
// pay for the expensive build when the source actually changed.
|
|
501
|
+
const { sourceHash } = await readArtifactStudioSource(dir);
|
|
502
|
+
if (!depsChanged && sourceHash === lastSourceHash)
|
|
503
|
+
return;
|
|
504
|
+
// Run the real build in a short-lived child process: repeated in-process
|
|
505
|
+
// Vite builds leak memory that GC can't reclaim and eventually OOM a
|
|
506
|
+
// long-running watch. Isolating each build keeps memory flat. The signal
|
|
507
|
+
// reaps the child if the watcher is shutting down mid-build.
|
|
508
|
+
const result = await buildArtifactStudioProjectIsolated(dir, { signal: controller.signal });
|
|
509
|
+
warnIfInlinedManifestDriftsFromBundle(result);
|
|
510
|
+
// The tree can change between the cheap pre-read above and the build
|
|
511
|
+
// finishing — including reverting back to the last-shipped snapshot. Trust
|
|
512
|
+
// the authoritative build hash and skip the redundant preview push when the
|
|
513
|
+
// source is unchanged — unless a linked dep changed, where the artifact
|
|
514
|
+
// source hash is identical but the bundled dependency code is not.
|
|
515
|
+
if (!depsChanged && result.sourceHash === lastSourceHash)
|
|
261
516
|
return;
|
|
262
|
-
lastSourceHash = result.sourceHash;
|
|
263
517
|
buildCount += 1;
|
|
518
|
+
// Resolve per push so the token refreshes across a long watch session.
|
|
519
|
+
const token = await getRequiredToken(args);
|
|
264
520
|
if (!projectId) {
|
|
265
|
-
const project = await ensureRemoteProject({ env: env.url, token, result, dir });
|
|
521
|
+
const project = await ensureRemoteProject({ env: env.url, token, result, linkConfigDir: dir });
|
|
266
522
|
projectId = project.id;
|
|
267
523
|
}
|
|
268
524
|
const deployment = await uploadDeployment({
|
|
@@ -272,25 +528,90 @@ async function devCommand(args) {
|
|
|
272
528
|
result,
|
|
273
529
|
channel: 'preview',
|
|
274
530
|
previewKey,
|
|
275
|
-
dir,
|
|
531
|
+
provenance: localGitMetadata(dir),
|
|
276
532
|
message: `Preview push ${buildCount}`,
|
|
277
533
|
});
|
|
278
|
-
|
|
534
|
+
// Mark this source as shipped only after the upload succeeds, so a failed
|
|
535
|
+
// build/upload leaves lastSourceHash stale and the next tick retries.
|
|
536
|
+
lastSourceHash = result.sourceHash;
|
|
537
|
+
console.log(`[seq-studio] preview ${deployment.version}`);
|
|
279
538
|
console.log(`${env.url}/artifact-studio/projects/${projectId}/preview?key=${encodeURIComponent(previewKey)}`);
|
|
280
539
|
}
|
|
281
|
-
|
|
282
|
-
|
|
540
|
+
// Best-effort liveness/build status to the dev server, so the preview page can
|
|
541
|
+
// show "live / watcher offline / build failed" instead of a fake spinner.
|
|
542
|
+
// Fire-and-forget: a heartbeat failure must never disrupt the watch loop.
|
|
543
|
+
async function sendHeartbeat(status) {
|
|
544
|
+
if (!projectId)
|
|
545
|
+
return;
|
|
546
|
+
try {
|
|
547
|
+
const token = await getRequiredToken(args);
|
|
548
|
+
await postJson({
|
|
549
|
+
baseUrl: env.url,
|
|
550
|
+
token,
|
|
551
|
+
path: `/api/artifact-studio/projects/${projectId}/preview-status?previewKey=${encodeURIComponent(previewKey)}`,
|
|
552
|
+
body: { buildOk: status.buildOk, error: status.error },
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// ignore — dev telemetry only
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Run one build/push and report status. Build failure is captured (not thrown)
|
|
560
|
+
// so the heartbeat still goes out as buildOk=false with the error message.
|
|
561
|
+
async function tickWithHeartbeat() {
|
|
562
|
+
let buildOk = true;
|
|
563
|
+
let buildError = null;
|
|
564
|
+
try {
|
|
565
|
+
await pushPreview();
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
buildOk = false;
|
|
569
|
+
buildError = error instanceof Error ? error.message : String(error);
|
|
570
|
+
console.error(buildError);
|
|
571
|
+
}
|
|
572
|
+
await sendHeartbeat({ buildOk, error: buildError });
|
|
573
|
+
}
|
|
574
|
+
// Surface sandbox traps once at startup (not per rebuild — that would spam the
|
|
575
|
+
// watch log). Re-run `dev`/`build`/`validate` to re-check after edits.
|
|
576
|
+
try {
|
|
577
|
+
const initial = await readArtifactStudioSource(dir);
|
|
578
|
+
reportSandboxWarnings(lintArtifactSandbox(initial.files));
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
// A malformed source surfaces its real error from the build below.
|
|
582
|
+
}
|
|
583
|
+
// One-shot push: no watcher, no lock.
|
|
584
|
+
if (once) {
|
|
585
|
+
await pushPreview();
|
|
283
586
|
return 0;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
587
|
+
}
|
|
588
|
+
// Single-instance guard: stop any watcher already running for this project and
|
|
589
|
+
// take over, so re-running `dev` never orphans a watcher that keeps racing on
|
|
590
|
+
// the shared token file.
|
|
591
|
+
const releaseLock = await acquireDevLock({ dir, previewKey });
|
|
592
|
+
const shutdown = (exitSignal) => {
|
|
593
|
+
controller.abort();
|
|
594
|
+
releaseLock();
|
|
595
|
+
if (exitSignal)
|
|
596
|
+
process.exit(0);
|
|
597
|
+
};
|
|
598
|
+
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
599
|
+
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
600
|
+
process.once('exit', () => releaseLock());
|
|
601
|
+
try {
|
|
602
|
+
await pushPreview();
|
|
603
|
+
await sendHeartbeat({ buildOk: true, error: null });
|
|
604
|
+
console.log('[seq-studio] watching for changes. Press Ctrl+C to stop.');
|
|
605
|
+
await runWatchLoop({
|
|
606
|
+
tick: tickWithHeartbeat,
|
|
607
|
+
intervalMs: 1500,
|
|
608
|
+
signal: controller.signal,
|
|
609
|
+
onError: (error) => console.error(error instanceof Error ? error.message : error),
|
|
292
610
|
});
|
|
293
|
-
}
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
releaseLock();
|
|
614
|
+
}
|
|
294
615
|
return 0;
|
|
295
616
|
}
|
|
296
617
|
async function promoteCommand(args) {
|
|
@@ -301,14 +622,14 @@ async function promoteCommand(args) {
|
|
|
301
622
|
const token = await getRequiredToken(args);
|
|
302
623
|
const config = await readLocalConfig();
|
|
303
624
|
if (!config.projectId)
|
|
304
|
-
throw new Error('Project is not linked. Run
|
|
625
|
+
throw new Error('Project is not linked. Run seq-studio artifact link.');
|
|
305
626
|
await postJson({
|
|
306
627
|
baseUrl: env.url,
|
|
307
628
|
token,
|
|
308
629
|
path: `/api/artifact-studio/projects/${config.projectId}/deployments/${deploymentId}/promote`,
|
|
309
630
|
body: {},
|
|
310
631
|
});
|
|
311
|
-
console.log(`[
|
|
632
|
+
console.log(`[seq-studio] promoted ${deploymentId}`);
|
|
312
633
|
return 0;
|
|
313
634
|
}
|
|
314
635
|
async function pullCommand(args) {
|
|
@@ -340,7 +661,7 @@ async function pullCommand(args) {
|
|
|
340
661
|
await mkdir(dirname(target), { recursive: true });
|
|
341
662
|
await writeFile(target, file.content, 'utf8');
|
|
342
663
|
}
|
|
343
|
-
console.log(`[
|
|
664
|
+
console.log(`[seq-studio] pulled ${projectId} to ${out}`);
|
|
344
665
|
return 0;
|
|
345
666
|
}
|
|
346
667
|
async function rollbackCommand(args) {
|
|
@@ -351,29 +672,45 @@ async function rollbackCommand(args) {
|
|
|
351
672
|
const token = await getRequiredToken(args);
|
|
352
673
|
const config = await readLocalConfig();
|
|
353
674
|
if (!config.projectId)
|
|
354
|
-
throw new Error('Project is not linked. Run
|
|
675
|
+
throw new Error('Project is not linked. Run seq-studio artifact link.');
|
|
355
676
|
await postJson({
|
|
356
677
|
baseUrl: env.url,
|
|
357
678
|
token,
|
|
358
679
|
path: `/api/artifact-studio/projects/${config.projectId}/deployments/${deploymentId}/rollback`,
|
|
359
680
|
body: {},
|
|
360
681
|
});
|
|
361
|
-
console.log(`[
|
|
682
|
+
console.log(`[seq-studio] rolled back to ${deploymentId}`);
|
|
362
683
|
return 0;
|
|
363
684
|
}
|
|
364
|
-
async function ensureRemoteProject({ env, token, result,
|
|
365
|
-
|
|
685
|
+
async function ensureRemoteProject({ env, token, result, linkConfigDir, allowCreate = true, explicitProjectId = null }) {
|
|
686
|
+
if (explicitProjectId) {
|
|
687
|
+
const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${explicitProjectId}` });
|
|
688
|
+
if (!data)
|
|
689
|
+
throw new Error(`--project ${explicitProjectId}: project not found on this environment`);
|
|
690
|
+
if (data.project.slug !== result.manifest.artifact.project_id) {
|
|
691
|
+
throw new Error(`--project ${explicitProjectId} resolves to slug "${data.project.slug}" but this source's ` +
|
|
692
|
+
`artifact.bundle.yml declares project_id "${result.manifest.artifact.project_id}" — refusing to deploy over a different project`);
|
|
693
|
+
}
|
|
694
|
+
return data.project;
|
|
695
|
+
}
|
|
696
|
+
const config = linkConfigDir ? await readLocalConfig(linkConfigDir) : {};
|
|
366
697
|
if (config.projectId) {
|
|
367
698
|
const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
|
|
368
699
|
if (data)
|
|
369
700
|
return data.project;
|
|
370
|
-
console.warn(`[
|
|
701
|
+
console.warn(`[seq-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
|
|
371
702
|
}
|
|
372
703
|
const existing = await fetchRemoteProject({ env, token, slug: result.manifest.artifact.project_id });
|
|
373
704
|
if (existing) {
|
|
374
|
-
|
|
705
|
+
if (linkConfigDir)
|
|
706
|
+
await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, linkConfigDir);
|
|
375
707
|
return existing;
|
|
376
708
|
}
|
|
709
|
+
if (!allowCreate) {
|
|
710
|
+
throw new Error(`project "${result.manifest.artifact.project_id}" does not exist on ${env} and --no-create is set. ` +
|
|
711
|
+
`First-time provisioning is a human step: run \`seq-studio artifact deploy . -e <env>\` once with your ` +
|
|
712
|
+
`own login (seqapi login) to create the project, then the CI sweep keeps it updated.`);
|
|
713
|
+
}
|
|
377
714
|
const created = await postJson({
|
|
378
715
|
baseUrl: env,
|
|
379
716
|
token,
|
|
@@ -384,13 +721,21 @@ async function ensureRemoteProject({ env, token, result, dir }) {
|
|
|
384
721
|
description: result.manifest.artifact.description ?? null,
|
|
385
722
|
},
|
|
386
723
|
});
|
|
387
|
-
|
|
724
|
+
if (linkConfigDir)
|
|
725
|
+
await writeLocalConfig({ ...config, projectId: created.project.id, projectSlug: created.project.slug }, linkConfigDir);
|
|
388
726
|
return created.project;
|
|
389
727
|
}
|
|
390
|
-
async function uploadDeployment({ env, token, projectId, result, channel, previewKey,
|
|
728
|
+
async function uploadDeployment({ env, token, projectId, result, channel, previewKey, provenance, message, }) {
|
|
729
|
+
// Active deploys omit `version`: the server derives it from its own
|
|
730
|
+
// sourceHash and de-dupes against the (projectId, version) unique
|
|
731
|
+
// constraint. Pinning it client-side is how a stale CLI-side hash turned
|
|
732
|
+
// redeploys into opaque 500s (unique violation on a version string minted
|
|
733
|
+
// from a hash the server no longer agrees with). Previews keep a
|
|
734
|
+
// timestamped version so successive pushes of identical source stay
|
|
735
|
+
// distinguishable.
|
|
391
736
|
const version = channel === 'preview'
|
|
392
737
|
? `preview-${Date.now().toString(36)}-${result.sourceHash.slice(0, 8)}`
|
|
393
|
-
:
|
|
738
|
+
: undefined;
|
|
394
739
|
const data = await postJson({
|
|
395
740
|
baseUrl: env,
|
|
396
741
|
token,
|
|
@@ -400,32 +745,59 @@ async function uploadDeployment({ env, token, projectId, result, channel, previe
|
|
|
400
745
|
files: result.files,
|
|
401
746
|
bundle: result.bundle,
|
|
402
747
|
sourceMap: result.sourceMap,
|
|
403
|
-
version,
|
|
748
|
+
...(version ? { version } : {}),
|
|
404
749
|
channel,
|
|
405
750
|
previewKey,
|
|
406
751
|
deployMessage: message ?? null,
|
|
407
|
-
...
|
|
752
|
+
...provenance,
|
|
408
753
|
},
|
|
409
754
|
});
|
|
410
755
|
return data.deployment;
|
|
411
756
|
}
|
|
412
|
-
async function fetchRemoteProject({ env, token, slug }) {
|
|
757
|
+
async function fetchRemoteProject({ env, token, slug, disambiguator }) {
|
|
413
758
|
const data = await getJson({ baseUrl: env, token, path: '/api/artifact-studio/projects' });
|
|
414
|
-
|
|
759
|
+
const matches = data.projects.filter((project) => project.slug === slug);
|
|
760
|
+
// Slugs are only unique per creator — a caller who can see several users'
|
|
761
|
+
// projects (notably the CI service account, which sees everything) can get
|
|
762
|
+
// multiple hits. Picking "whichever sorts first" would deploy over an
|
|
763
|
+
// arbitrary project, so ambiguity is a hard error with the disambiguator.
|
|
764
|
+
if (matches.length > 1) {
|
|
765
|
+
throw new Error(`${matches.length} projects with slug "${slug}" exist on this environment ` +
|
|
766
|
+
`(ids: ${matches.map((project) => project.id).join(', ')}) — ` +
|
|
767
|
+
(disambiguator ??
|
|
768
|
+
'target one explicitly with --project <id> (deploy) or `seq-studio artifact link . --project <id>`.'));
|
|
769
|
+
}
|
|
770
|
+
return matches[0] ?? null;
|
|
415
771
|
}
|
|
416
772
|
async function envFromArgs(args) {
|
|
417
773
|
const config = await readLocalConfig().catch(() => ({ defaultEnv: undefined }));
|
|
418
774
|
return resolveEnvironment(typeof args.flags.env === 'string' ? args.flags.env : undefined, config.defaultEnv);
|
|
419
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Optional refreshing token provider, injected by an embedder (e.g. seq-studio)
|
|
778
|
+
* that owns the shared seqapi token file and can mint a fresh access token on
|
|
779
|
+
* demand. Consulted before the static ARTIFACT_STUDIO_TOKEN env var so a
|
|
780
|
+
* long-running `dev` watcher keeps deploying past the initial token's TTL —
|
|
781
|
+
* the env var is captured once at process start and never refreshes.
|
|
782
|
+
*/
|
|
783
|
+
let tokenProvider = null;
|
|
784
|
+
export function setTokenProvider(provider) {
|
|
785
|
+
tokenProvider = provider;
|
|
786
|
+
}
|
|
420
787
|
async function getRequiredToken(args) {
|
|
421
788
|
const token = await getOptionalToken(args);
|
|
422
789
|
if (!token)
|
|
423
|
-
throw new Error('Missing token. Run
|
|
790
|
+
throw new Error('Missing token. Run seqapi login or pass --token.');
|
|
424
791
|
return token;
|
|
425
792
|
}
|
|
426
793
|
async function getOptionalToken(args) {
|
|
427
794
|
if (typeof args.flags.token === 'string')
|
|
428
795
|
return args.flags.token;
|
|
796
|
+
if (tokenProvider) {
|
|
797
|
+
const provided = await tokenProvider();
|
|
798
|
+
if (provided)
|
|
799
|
+
return provided;
|
|
800
|
+
}
|
|
429
801
|
if (process.env.ARTIFACT_STUDIO_TOKEN)
|
|
430
802
|
return process.env.ARTIFACT_STUDIO_TOKEN;
|
|
431
803
|
return getAccessToken();
|
|
@@ -434,31 +806,27 @@ async function replaceInFile(path, pattern, value) {
|
|
|
434
806
|
const content = await import('node:fs/promises').then((fs) => fs.readFile(path, 'utf8'));
|
|
435
807
|
await writeFile(path, content.replace(pattern, value), 'utf8');
|
|
436
808
|
}
|
|
437
|
-
function gitMetadata(dir) {
|
|
438
|
-
const gitOpts = {
|
|
439
|
-
cwd: dir,
|
|
440
|
-
encoding: 'utf8',
|
|
441
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
442
|
-
};
|
|
443
|
-
try {
|
|
444
|
-
const gitCommit = String(execFileSync('git', ['rev-parse', 'HEAD'], gitOpts)).trim();
|
|
445
|
-
const gitBranch = String(execFileSync('git', ['branch', '--show-current'], gitOpts)).trim();
|
|
446
|
-
const gitDirty = String(execFileSync('git', ['status', '--porcelain'], gitOpts)).trim().length > 0;
|
|
447
|
-
return { gitCommit, gitBranch, gitDirty };
|
|
448
|
-
}
|
|
449
|
-
catch {
|
|
450
|
-
return { gitCommit: null, gitBranch: null, gitDirty: null };
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
809
|
function slugify(value) {
|
|
454
810
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'my-artifact';
|
|
455
811
|
}
|
|
456
812
|
function titleize(slug) {
|
|
457
813
|
return slug.split('-').map((part) => part ? part[0].toUpperCase() + part.slice(1) : '').join(' ');
|
|
458
814
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
815
|
+
function warnIfInlinedManifestDriftsFromBundle(result) {
|
|
816
|
+
const bundleFile = result.files.find((file) => file.path === 'artifact.bundle.yml');
|
|
817
|
+
if (!bundleFile)
|
|
818
|
+
return;
|
|
819
|
+
try {
|
|
820
|
+
const fromBundle = parseArtifactStudioManifest(yaml.load(bundleFile.content));
|
|
821
|
+
if (fromBundle.sidebarBadge && !result.manifest.sidebarBadge) {
|
|
822
|
+
console.warn('[seq-studio] warning: artifact.bundle.yml declares sidebarBadge but the deploy payload omits it — upgrade @sequenceholdings/artifact-studio / seq-studio');
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// validate/build already surfaced parse errors
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
// This module is a pure library export. `seq-studio artifact` imports
|
|
830
|
+
// `runCli` from `@sequenceholdings/artifact-studio/cli` (see
|
|
831
|
+
// shared/services/studio-cli/README.md) and invokes it after injecting a
|
|
464
832
|
// shared base URL + Bearer token.
|