@sequenceholdings/artifact-studio 0.1.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.
Files changed (39) hide show
  1. package/dist/api.d.ts +14 -0
  2. package/dist/api.js +49 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +129 -0
  5. package/dist/build.d.ts +12 -0
  6. package/dist/build.js +134 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +475 -0
  9. package/dist/config.d.ts +23 -0
  10. package/dist/config.js +48 -0
  11. package/dist/hash.d.ts +10 -0
  12. package/dist/hash.js +36 -0
  13. package/dist/manifest.d.ts +44 -0
  14. package/dist/manifest.js +45 -0
  15. package/dist/paths.d.ts +6 -0
  16. package/dist/paths.js +34 -0
  17. package/dist/project.d.ts +14 -0
  18. package/dist/project.js +76 -0
  19. package/dist/sdk.d.ts +60 -0
  20. package/dist/sdk.js +4 -0
  21. package/dist/templates/react-vite/CLAUDE.md +55 -0
  22. package/dist/templates/react-vite/artifact.bundle.yml +30 -0
  23. package/dist/templates/react-vite/index.html +12 -0
  24. package/dist/templates/react-vite/package.json +27 -0
  25. package/dist/templates/react-vite/src/App.tsx +54 -0
  26. package/dist/templates/react-vite/src/lib/api.ts +13 -0
  27. package/dist/templates/react-vite/src/main.tsx +18 -0
  28. package/dist/templates/react-vite/src/styles.css +3 -0
  29. package/dist/templates/react-vite/vite.config.ts +7 -0
  30. package/package.json +50 -0
  31. package/templates/react-vite/CLAUDE.md +55 -0
  32. package/templates/react-vite/artifact.bundle.yml +30 -0
  33. package/templates/react-vite/index.html +12 -0
  34. package/templates/react-vite/package.json +27 -0
  35. package/templates/react-vite/src/App.tsx +54 -0
  36. package/templates/react-vite/src/lib/api.ts +13 -0
  37. package/templates/react-vite/src/main.tsx +18 -0
  38. package/templates/react-vite/src/styles.css +3 -0
  39. package/templates/react-vite/vite.config.ts +7 -0
package/dist/cli.js ADDED
@@ -0,0 +1,475 @@
1
+ #!/usr/bin/env node
2
+ import { cp, mkdir, writeFile } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, join, relative, resolve } from 'node:path';
6
+ import { execFileSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { getJson, getJsonOr404, postJson } from './api.js';
9
+ import { readLocalConfig, resolveEnvironment, writeLocalConfig, writeTokenConfig, } from './config.js';
10
+ import { buildArtifactStudioProject, writeBuildArtifact } from './build.js';
11
+ import { hasArtifactStudioManifest, readArtifactStudioSource } from './project.js';
12
+ import { getAccessToken, loginWithPkce } from './auth.js';
13
+ const COMMANDS = [
14
+ 'init', 'login', 'logout', 'whoami', 'link', 'env', 'status',
15
+ 'validate', 'build', 'plan', 'deploy', 'dev', 'promote', 'pull', 'rollback',
16
+ ];
17
+ const USAGE = `usage:
18
+ artifact-studio init <dir>
19
+ artifact-studio login [--token <token>]
20
+ artifact-studio logout
21
+ artifact-studio whoami --env <env>
22
+ artifact-studio link --env <env> [--project <id>]
23
+ artifact-studio env list|use <env>
24
+ artifact-studio status [--env <env>]
25
+ artifact-studio validate [dir]
26
+ artifact-studio build [dir] [--out dist/artifact-bundle.json]
27
+ artifact-studio plan [dir] --env <env>
28
+ artifact-studio deploy [dir] --env <env>
29
+ artifact-studio dev [dir] --env <env> [--once] [--key default]
30
+ artifact-studio promote <deployment-id> --env <env>
31
+ artifact-studio pull <project-id> --env <env> --out <dir>
32
+ artifact-studio rollback <deployment-id> --env <env>`;
33
+ export async function runCli(argv = process.argv.slice(2)) {
34
+ const parsed = parseArgs(argv);
35
+ if ('error' in parsed) {
36
+ console.error(parsed.error);
37
+ return 1;
38
+ }
39
+ switch (parsed.command) {
40
+ case 'init': return initCommand(parsed);
41
+ case 'login': return loginCommand(parsed);
42
+ case 'logout': return logoutCommand();
43
+ case 'whoami': return whoamiCommand(parsed);
44
+ case 'link': return linkCommand(parsed);
45
+ case 'env': return envCommand(parsed);
46
+ case 'status': return statusCommand(parsed);
47
+ case 'validate': return validateCommand(parsed);
48
+ case 'build': return buildCommand(parsed);
49
+ case 'plan': return planCommand(parsed);
50
+ case 'deploy': return deployCommand(parsed);
51
+ case 'dev': return devCommand(parsed);
52
+ case 'promote': return promoteCommand(parsed);
53
+ case 'pull': return pullCommand(parsed);
54
+ case 'rollback': return rollbackCommand(parsed);
55
+ }
56
+ }
57
+ function parseArgs(argv) {
58
+ if (argv[0] === '--')
59
+ argv = argv.slice(1);
60
+ const [rawCommand, ...rest] = argv;
61
+ if (!rawCommand || !COMMANDS.includes(rawCommand))
62
+ return { error: USAGE };
63
+ const positional = [];
64
+ const flags = {};
65
+ for (let i = 0; i < rest.length; i++) {
66
+ const arg = rest[i];
67
+ if (arg.startsWith('--')) {
68
+ const key = arg.slice(2);
69
+ const next = rest[i + 1];
70
+ if (next && !next.startsWith('--')) {
71
+ flags[key] = next;
72
+ i += 1;
73
+ }
74
+ else {
75
+ flags[key] = true;
76
+ }
77
+ }
78
+ else {
79
+ positional.push(arg);
80
+ }
81
+ }
82
+ return { command: rawCommand, positional, flags };
83
+ }
84
+ async function initCommand(args) {
85
+ const dir = resolve(args.positional[0] ?? '.');
86
+ const name = dir.split('/').filter(Boolean).at(-1) ?? 'my-artifact';
87
+ const slug = slugify(name);
88
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
89
+ const templateRoot = existsSync(resolve(moduleDir, 'templates', 'react-vite'))
90
+ ? resolve(moduleDir, 'templates', 'react-vite')
91
+ : resolve(moduleDir, '..', 'templates', 'react-vite');
92
+ await mkdir(dir, { recursive: true });
93
+ await cp(templateRoot, dir, { recursive: true, force: false, errorOnExist: false });
94
+ await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{slug\}\}/g, slug);
95
+ await replaceInFile(join(dir, 'artifact.bundle.yml'), /\{\{title\}\}/g, titleize(slug));
96
+ await replaceInFile(join(dir, 'package.json'), /\{\{slug\}\}/g, slug);
97
+ // TODO(npm-publish): switch back to ^semver from the registry once both
98
+ // @sequenceholdings/atlas-ui and @sequenceholdings/artifact-studio are published to npm.
99
+ const pkgJson = join(dir, 'package.json');
100
+ await replaceInFile(pkgJson, /"@sequenceholdings\/atlas-ui": "workspace:\*"/, `"@sequenceholdings/atlas-ui": ${JSON.stringify(`link:${resolveAtlasUiLocalPath()}`)}`);
101
+ await replaceInFile(pkgJson, /"@sequenceholdings\/artifact-studio": "workspace:\*"/, `"@sequenceholdings/artifact-studio": ${JSON.stringify(`link:${resolveArtifactStudioLocalPath()}`)}`);
102
+ console.log(`[artifact-studio] initialized ${dir}`);
103
+ const relativeDir = relative(process.cwd(), dir) || '.';
104
+ console.log(`Next: cd ${relativeDir} && pnpm install && artifact-studio login && artifact-studio dev --env staging`);
105
+ return 0;
106
+ }
107
+ function resolveAtlasUiLocalPath() {
108
+ const req = createRequire(import.meta.url);
109
+ return dirname(req.resolve('@sequenceholdings/atlas-ui/package.json'));
110
+ }
111
+ function resolveArtifactStudioLocalPath() {
112
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..');
113
+ }
114
+ async function loginCommand(args) {
115
+ const token = typeof args.flags.token === 'string' ? args.flags.token : undefined;
116
+ if (token) {
117
+ await writeTokenConfig({ accessToken: token });
118
+ console.log('[artifact-studio] saved access token');
119
+ return 0;
120
+ }
121
+ await loginWithPkce();
122
+ console.log('[artifact-studio] logged in');
123
+ return 0;
124
+ }
125
+ async function logoutCommand() {
126
+ await writeTokenConfig({});
127
+ console.log('[artifact-studio] logged out');
128
+ return 0;
129
+ }
130
+ async function whoamiCommand(args) {
131
+ const env = await envFromArgs(args);
132
+ const token = await getRequiredToken(args);
133
+ const data = await getJson({
134
+ baseUrl: env.url,
135
+ token,
136
+ path: '/api/auth/me',
137
+ }).catch(async () => {
138
+ const projects = await getJson({ baseUrl: env.url, token, path: '/api/artifact-studio/projects' });
139
+ return { subject: `authenticated (${projects.projects.length} visible project(s))` };
140
+ });
141
+ console.log('user' in data ? (data.user?.email ?? data.user?.sub ?? 'authenticated') : data.subject);
142
+ return 0;
143
+ }
144
+ async function linkCommand(args) {
145
+ const dir = resolve(args.positional[0] ?? '.');
146
+ const env = await envFromArgs(args);
147
+ const token = await getRequiredToken(args);
148
+ const source = await readArtifactStudioSource(dir);
149
+ const requestedProjectId = typeof args.flags.project === 'string' ? args.flags.project : undefined;
150
+ let project = null;
151
+ if (requestedProjectId) {
152
+ const data = await getJson({ baseUrl: env.url, token, path: `/api/artifact-studio/projects/${requestedProjectId}` });
153
+ project = data.project;
154
+ }
155
+ else {
156
+ project = await fetchRemoteProject({ env: env.url, token, slug: source.manifest.artifact.project_id });
157
+ }
158
+ if (!project) {
159
+ const created = await postJson({
160
+ baseUrl: env.url,
161
+ token,
162
+ path: '/api/artifact-studio/projects',
163
+ body: {
164
+ title: source.manifest.artifact.title,
165
+ slug: source.manifest.artifact.project_id,
166
+ description: source.manifest.artifact.description ?? null,
167
+ },
168
+ });
169
+ project = created.project;
170
+ }
171
+ await writeLocalConfig({
172
+ projectId: project.id,
173
+ projectSlug: project.slug,
174
+ defaultEnv: env.name,
175
+ previewKey: 'default',
176
+ }, dir);
177
+ console.log(`[artifact-studio] linked ${project.title} (${project.id})`);
178
+ return 0;
179
+ }
180
+ async function envCommand(args) {
181
+ const [subcommand, value] = args.positional;
182
+ if (subcommand === 'list') {
183
+ console.log('local\nstaging\nproduction\nbanksouth');
184
+ return 0;
185
+ }
186
+ if (subcommand === 'use') {
187
+ const env = resolveEnvironment(value);
188
+ const config = await readLocalConfig();
189
+ await writeLocalConfig({ ...config, defaultEnv: env.name });
190
+ console.log(`[artifact-studio] default env set to ${env.name}`);
191
+ return 0;
192
+ }
193
+ console.error('artifact-studio env list|use <env>');
194
+ return 1;
195
+ }
196
+ async function statusCommand(args) {
197
+ const env = await envFromArgs(args);
198
+ const config = await readLocalConfig();
199
+ const source = hasArtifactStudioManifest('.') ? await readArtifactStudioSource('.') : null;
200
+ console.log(`env: ${env.name}`);
201
+ console.log(`url: ${env.url}`);
202
+ console.log(`projectId: ${config.projectId ?? '(not linked)'}`);
203
+ if (source)
204
+ console.log(`source: ${source.sourceHash}`);
205
+ if (env.name === 'local') {
206
+ await fetch(`${env.url}/api/health`)
207
+ .then((response) => console.log(`local Atlas: ${response.ok ? 'ready' : `HTTP ${response.status}`}`))
208
+ .catch(() => console.log('local Atlas: not reachable (start Atlas on port 5001 or pass --env staging)'));
209
+ }
210
+ return 0;
211
+ }
212
+ async function validateCommand(args) {
213
+ const dir = resolve(args.positional[0] ?? '.');
214
+ const source = await readArtifactStudioSource(dir);
215
+ console.log(`[artifact-studio] valid ${source.manifest.bundle.name}`);
216
+ console.log(`[artifact-studio] files: ${source.files.length}`);
217
+ console.log(`[artifact-studio] source: ${source.sourceHash}`);
218
+ return 0;
219
+ }
220
+ async function buildCommand(args) {
221
+ const dir = resolve(args.positional[0] ?? '.');
222
+ const result = await buildArtifactStudioProject(dir);
223
+ const out = typeof args.flags.out === 'string' ? resolve(args.flags.out) : resolve(dir, 'dist', 'artifact-bundle.json');
224
+ await writeBuildArtifact(result, out);
225
+ console.log(`[artifact-studio] built ${result.manifest.bundle.name}`);
226
+ console.log(`[artifact-studio] source: ${result.sourceHash}`);
227
+ console.log(`[artifact-studio] bundle: ${result.bundleHash}`);
228
+ console.log(`[artifact-studio] wrote ${out}`);
229
+ return 0;
230
+ }
231
+ async function planCommand(args) {
232
+ const dir = resolve(args.positional[0] ?? '.');
233
+ const env = await envFromArgs(args);
234
+ const token = await getOptionalToken(args);
235
+ const result = await buildArtifactStudioProject(dir);
236
+ console.log(`[artifact-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
237
+ console.log(` source: ${result.sourceHash}`);
238
+ console.log(` bundle: ${result.bundleHash}`);
239
+ if (!token) {
240
+ console.log(' remote: skipped (run artifact-studio login)');
241
+ return 0;
242
+ }
243
+ const remote = await fetchRemoteProject({ env: env.url, token, slug: result.manifest.artifact.project_id });
244
+ if (!remote)
245
+ console.log(' action: create project and deploy');
246
+ else if (remote.activeDeployment?.sourceHash === result.sourceHash)
247
+ console.log(' action: no-op');
248
+ else
249
+ console.log(` action: deploy over ${remote.activeDeployment?.version ?? 'no active deployment'}`);
250
+ return 0;
251
+ }
252
+ async function deployCommand(args) {
253
+ const dir = resolve(args.positional[0] ?? '.');
254
+ const env = await envFromArgs(args);
255
+ const token = await getRequiredToken(args);
256
+ const result = await buildArtifactStudioProject(dir);
257
+ const project = await ensureRemoteProject({ env: env.url, token, result, dir });
258
+ const deployment = await uploadDeployment({ env: env.url, token, projectId: project.id, result, channel: 'active', dir });
259
+ console.log(`[artifact-studio] deployed ${project.title} ${deployment.version}`);
260
+ console.log(`${env.url}/artifact-studio/projects/${project.id}/deployments/${deployment.id}`);
261
+ return 0;
262
+ }
263
+ async function devCommand(args) {
264
+ const dir = resolve(args.positional[0] ?? '.');
265
+ const env = await envFromArgs(args);
266
+ const token = await getRequiredToken(args);
267
+ const config = await readLocalConfig(dir);
268
+ const previewKey = typeof args.flags.key === 'string' ? args.flags.key : config.previewKey ?? 'default';
269
+ const once = args.flags.once === true;
270
+ let projectId = null;
271
+ let lastSourceHash = null;
272
+ let buildCount = 0;
273
+ async function pushPreview() {
274
+ const result = await buildArtifactStudioProject(dir);
275
+ if (result.sourceHash === lastSourceHash)
276
+ return;
277
+ lastSourceHash = result.sourceHash;
278
+ buildCount += 1;
279
+ if (!projectId) {
280
+ const project = await ensureRemoteProject({ env: env.url, token, result, dir });
281
+ projectId = project.id;
282
+ }
283
+ const deployment = await uploadDeployment({
284
+ env: env.url,
285
+ token,
286
+ projectId,
287
+ result,
288
+ channel: 'preview',
289
+ previewKey,
290
+ dir,
291
+ message: `Preview push ${buildCount}`,
292
+ });
293
+ console.log(`[artifact-studio] preview ${deployment.version}`);
294
+ console.log(`${env.url}/artifact-studio/projects/${projectId}/preview?key=${encodeURIComponent(previewKey)}`);
295
+ }
296
+ await pushPreview();
297
+ if (once)
298
+ return 0;
299
+ console.log('[artifact-studio] watching for changes. Press Ctrl+C to stop.');
300
+ await new Promise((resolvePromise) => {
301
+ const interval = setInterval(() => {
302
+ pushPreview().catch((error) => console.error(error instanceof Error ? error.message : error));
303
+ }, 1500);
304
+ process.on('SIGINT', () => {
305
+ clearInterval(interval);
306
+ resolvePromise();
307
+ });
308
+ });
309
+ return 0;
310
+ }
311
+ async function promoteCommand(args) {
312
+ const [deploymentId] = args.positional;
313
+ if (!deploymentId)
314
+ throw new Error('promote requires <deployment-id>');
315
+ const env = await envFromArgs(args);
316
+ const token = await getRequiredToken(args);
317
+ const config = await readLocalConfig();
318
+ if (!config.projectId)
319
+ throw new Error('Project is not linked. Run artifact-studio link.');
320
+ await postJson({
321
+ baseUrl: env.url,
322
+ token,
323
+ path: `/api/artifact-studio/projects/${config.projectId}/deployments/${deploymentId}/promote`,
324
+ body: {},
325
+ });
326
+ console.log(`[artifact-studio] promoted ${deploymentId}`);
327
+ return 0;
328
+ }
329
+ async function pullCommand(args) {
330
+ const [projectId] = args.positional;
331
+ if (!projectId)
332
+ throw new Error('pull requires <project-id>');
333
+ const env = await envFromArgs(args);
334
+ const token = await getRequiredToken(args);
335
+ const out = typeof args.flags.out === 'string' ? resolve(args.flags.out) : resolve(projectId);
336
+ const projectData = await getJson({
337
+ baseUrl: env.url,
338
+ token,
339
+ path: `/api/artifact-studio/projects/${projectId}`,
340
+ });
341
+ if (!projectData.project.activeDeploymentId) {
342
+ throw new Error('Project has no active deployment to pull.');
343
+ }
344
+ const deploymentData = await getJson({
345
+ baseUrl: env.url,
346
+ token,
347
+ path: `/api/artifact-studio/projects/${projectId}/deployments/${projectData.project.activeDeploymentId}`,
348
+ });
349
+ for (const file of deploymentData.deployment.files) {
350
+ const target = join(out, file.path);
351
+ await mkdir(dirname(target), { recursive: true });
352
+ await writeFile(target, file.content, 'utf8');
353
+ }
354
+ console.log(`[artifact-studio] pulled ${projectId} to ${out}`);
355
+ return 0;
356
+ }
357
+ async function rollbackCommand(args) {
358
+ const [deploymentId] = args.positional;
359
+ if (!deploymentId)
360
+ throw new Error('rollback requires <deployment-id>');
361
+ const env = await envFromArgs(args);
362
+ const token = await getRequiredToken(args);
363
+ const config = await readLocalConfig();
364
+ if (!config.projectId)
365
+ throw new Error('Project is not linked. Run artifact-studio link.');
366
+ await postJson({
367
+ baseUrl: env.url,
368
+ token,
369
+ path: `/api/artifact-studio/projects/${config.projectId}/deployments/${deploymentId}/rollback`,
370
+ body: {},
371
+ });
372
+ console.log(`[artifact-studio] rolled back to ${deploymentId}`);
373
+ return 0;
374
+ }
375
+ async function ensureRemoteProject({ env, token, result, dir }) {
376
+ const config = await readLocalConfig(dir);
377
+ if (config.projectId) {
378
+ const data = await getJsonOr404({ baseUrl: env, token, path: `/api/artifact-studio/projects/${config.projectId}` });
379
+ if (data)
380
+ return data.project;
381
+ console.warn(`[artifact-studio] cached projectId ${config.projectId} not found on ${env} — relinking by slug`);
382
+ }
383
+ const existing = await fetchRemoteProject({ env, token, slug: result.manifest.artifact.project_id });
384
+ if (existing) {
385
+ await writeLocalConfig({ ...config, projectId: existing.id, projectSlug: existing.slug }, dir);
386
+ return existing;
387
+ }
388
+ const created = await postJson({
389
+ baseUrl: env,
390
+ token,
391
+ path: '/api/artifact-studio/projects',
392
+ body: {
393
+ title: result.manifest.artifact.title,
394
+ slug: result.manifest.artifact.project_id,
395
+ description: result.manifest.artifact.description ?? null,
396
+ },
397
+ });
398
+ await writeLocalConfig({ ...config, projectId: created.project.id, projectSlug: created.project.slug }, dir);
399
+ return created.project;
400
+ }
401
+ async function uploadDeployment({ env, token, projectId, result, channel, previewKey, dir, message, }) {
402
+ const version = channel === 'preview'
403
+ ? `preview-${Date.now().toString(36)}-${result.sourceHash.slice(0, 8)}`
404
+ : result.sourceHash.slice(0, 12);
405
+ const data = await postJson({
406
+ baseUrl: env,
407
+ token,
408
+ path: `/api/artifact-studio/projects/${projectId}/deployments`,
409
+ body: {
410
+ manifest: result.manifest,
411
+ files: result.files,
412
+ bundle: result.bundle,
413
+ sourceMap: result.sourceMap,
414
+ version,
415
+ channel,
416
+ previewKey,
417
+ deployMessage: message ?? null,
418
+ ...gitMetadata(dir),
419
+ },
420
+ });
421
+ return data.deployment;
422
+ }
423
+ async function fetchRemoteProject({ env, token, slug }) {
424
+ const data = await getJson({ baseUrl: env, token, path: '/api/artifact-studio/projects' });
425
+ return data.projects.find((project) => project.slug === slug) ?? null;
426
+ }
427
+ async function envFromArgs(args) {
428
+ const config = await readLocalConfig().catch(() => ({ defaultEnv: undefined }));
429
+ return resolveEnvironment(typeof args.flags.env === 'string' ? args.flags.env : undefined, config.defaultEnv);
430
+ }
431
+ async function getRequiredToken(args) {
432
+ const token = await getOptionalToken(args);
433
+ if (!token)
434
+ throw new Error('Missing token. Run artifact-studio login or pass --token.');
435
+ return token;
436
+ }
437
+ async function getOptionalToken(args) {
438
+ if (typeof args.flags.token === 'string')
439
+ return args.flags.token;
440
+ if (process.env.ARTIFACT_STUDIO_TOKEN)
441
+ return process.env.ARTIFACT_STUDIO_TOKEN;
442
+ return getAccessToken();
443
+ }
444
+ async function replaceInFile(path, pattern, value) {
445
+ const content = await import('node:fs/promises').then((fs) => fs.readFile(path, 'utf8'));
446
+ await writeFile(path, content.replace(pattern, value), 'utf8');
447
+ }
448
+ function gitMetadata(dir) {
449
+ const gitOpts = {
450
+ cwd: dir,
451
+ encoding: 'utf8',
452
+ stdio: ['ignore', 'pipe', 'ignore'],
453
+ };
454
+ try {
455
+ const gitCommit = String(execFileSync('git', ['rev-parse', 'HEAD'], gitOpts)).trim();
456
+ const gitBranch = String(execFileSync('git', ['branch', '--show-current'], gitOpts)).trim();
457
+ const gitDirty = String(execFileSync('git', ['status', '--porcelain'], gitOpts)).trim().length > 0;
458
+ return { gitCommit, gitBranch, gitDirty };
459
+ }
460
+ catch {
461
+ return { gitCommit: null, gitBranch: null, gitDirty: null };
462
+ }
463
+ }
464
+ function slugify(value) {
465
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'my-artifact';
466
+ }
467
+ function titleize(slug) {
468
+ return slug.split('-').map((part) => part ? part[0].toUpperCase() + part.slice(1) : '').join(' ');
469
+ }
470
+ runCli()
471
+ .then((code) => process.exit(code))
472
+ .catch((error) => {
473
+ console.error(error instanceof Error ? error.message : error);
474
+ process.exit(1);
475
+ });
@@ -0,0 +1,23 @@
1
+ export declare const ENV_URLS: Record<string, string>;
2
+ export interface LocalProjectConfig {
3
+ projectId?: string;
4
+ projectSlug?: string;
5
+ defaultEnv?: string;
6
+ previewKey?: string;
7
+ }
8
+ export interface TokenConfig {
9
+ accessToken?: string;
10
+ refreshToken?: string;
11
+ expiresAt?: number;
12
+ }
13
+ export declare function globalConfigDir(): string;
14
+ export declare function tokenConfigPath(): string;
15
+ export declare function localConfigPath(cwd?: string): string;
16
+ export declare function readLocalConfig(cwd?: string): Promise<LocalProjectConfig>;
17
+ export declare function writeLocalConfig(config: LocalProjectConfig, cwd?: string): Promise<void>;
18
+ export declare function readTokenConfig(): Promise<TokenConfig>;
19
+ export declare function writeTokenConfig(config: TokenConfig): Promise<void>;
20
+ export declare function resolveEnvironment(name: string | undefined, fallback?: string): {
21
+ name: string;
22
+ url: string;
23
+ };
package/dist/config.js ADDED
@@ -0,0 +1,48 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ export const ENV_URLS = {
6
+ local: 'http://localhost:5001',
7
+ staging: 'https://staging.atlas.seqholdings.com',
8
+ production: 'https://atlas.seqholdings.com',
9
+ banksouth: 'https://banksouth.seqholdings.com',
10
+ };
11
+ export function globalConfigDir() {
12
+ return join(homedir(), '.config', 'sequence-artifact-studio');
13
+ }
14
+ export function tokenConfigPath() {
15
+ return join(globalConfigDir(), 'tokens.json');
16
+ }
17
+ export function localConfigPath(cwd = process.cwd()) {
18
+ return join(resolve(cwd), '.artifact-studio', 'config.json');
19
+ }
20
+ export async function readLocalConfig(cwd = process.cwd()) {
21
+ const path = localConfigPath(cwd);
22
+ if (!existsSync(path))
23
+ return {};
24
+ return JSON.parse(await readFile(path, 'utf8'));
25
+ }
26
+ export async function writeLocalConfig(config, cwd = process.cwd()) {
27
+ const path = localConfigPath(cwd);
28
+ await mkdir(dirname(path), { recursive: true });
29
+ await writeFile(path, JSON.stringify(config, null, 2) + '\n', 'utf8');
30
+ }
31
+ export async function readTokenConfig() {
32
+ const path = tokenConfigPath();
33
+ if (!existsSync(path))
34
+ return {};
35
+ return JSON.parse(await readFile(path, 'utf8'));
36
+ }
37
+ export async function writeTokenConfig(config) {
38
+ const path = tokenConfigPath();
39
+ await mkdir(dirname(path), { recursive: true });
40
+ await writeFile(path, JSON.stringify(config, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
41
+ }
42
+ export function resolveEnvironment(name, fallback) {
43
+ const envName = name ?? fallback;
44
+ const url = envName ? ENV_URLS[envName] : undefined;
45
+ if (!envName || !url)
46
+ throw new Error('--env must be one of: local, staging, production, banksouth');
47
+ return { name: envName, url };
48
+ }
package/dist/hash.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export interface HashableArtifactFile {
2
+ path: string;
3
+ content: string;
4
+ encoding?: string;
5
+ }
6
+ export declare function sha256Hex(content: string | Buffer): string;
7
+ export declare function computeSourceHash({ files, manifest, }: {
8
+ files: readonly HashableArtifactFile[];
9
+ manifest: unknown;
10
+ }): string;
package/dist/hash.js ADDED
@@ -0,0 +1,36 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { normalizeArtifactPath } from './paths.js';
3
+ export function sha256Hex(content) {
4
+ return createHash('sha256').update(content).digest('hex');
5
+ }
6
+ function stableJson(value) {
7
+ if (value === null || typeof value !== 'object')
8
+ return JSON.stringify(value);
9
+ if (Array.isArray(value))
10
+ return `[${value.map(stableJson).join(',')}]`;
11
+ const obj = value;
12
+ return `{${Object.keys(obj)
13
+ .sort()
14
+ .map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`)
15
+ .join(',')}}`;
16
+ }
17
+ export function computeSourceHash({ files, manifest, }) {
18
+ const h = createHash('sha256');
19
+ h.update('artifact-studio-source-v2\0');
20
+ h.update(stableJson(manifest));
21
+ h.update('\0');
22
+ const sorted = [...files].map((file) => ({
23
+ path: normalizeArtifactPath(file.path),
24
+ content: file.content,
25
+ encoding: file.encoding ?? 'utf8',
26
+ })).sort((a, b) => a.path.localeCompare(b.path));
27
+ for (const file of sorted) {
28
+ h.update(file.path);
29
+ h.update('\0');
30
+ h.update(file.encoding);
31
+ h.update('\0');
32
+ h.update(sha256Hex(file.content));
33
+ h.update('\0');
34
+ }
35
+ return h.digest('hex');
36
+ }
@@ -0,0 +1,44 @@
1
+ import { z } from 'zod';
2
+ export declare const artifactStudioManifestSchema: z.ZodObject<{
3
+ bundle: z.ZodObject<{
4
+ name: z.ZodString;
5
+ schema_version: z.ZodDefault<z.ZodLiteral<1>>;
6
+ }, z.core.$strip>;
7
+ artifact: z.ZodObject<{
8
+ project_id: z.ZodString;
9
+ title: z.ZodString;
10
+ description: z.ZodOptional<z.ZodString>;
11
+ type: z.ZodDefault<z.ZodEnum<{
12
+ react: "react";
13
+ html: "html";
14
+ }>>;
15
+ entrypoint: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
16
+ }, z.core.$strip>;
17
+ runtime: z.ZodDefault<z.ZodObject<{
18
+ sdk: z.ZodDefault<z.ZodLiteral<"sequence">>;
19
+ react: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<18>, z.ZodLiteral<19>]>>;
20
+ }, z.core.$strip>>;
21
+ capabilities: z.ZodDefault<z.ZodObject<{
22
+ data: z.ZodDefault<z.ZodObject<{
23
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
24
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
25
+ }, z.core.$strip>>;
26
+ api: z.ZodDefault<z.ZodObject<{
27
+ read: z.ZodDefault<z.ZodArray<z.ZodString>>;
28
+ write: z.ZodDefault<z.ZodArray<z.ZodString>>;
29
+ }, z.core.$strip>>;
30
+ functions: z.ZodDefault<z.ZodObject<{
31
+ invoke: z.ZodDefault<z.ZodArray<z.ZodString>>;
32
+ }, z.core.$strip>>;
33
+ }, z.core.$strip>>;
34
+ targets: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
35
+ url: z.ZodString;
36
+ visibility: z.ZodDefault<z.ZodEnum<{
37
+ private: "private";
38
+ shared: "shared";
39
+ published: "published";
40
+ }>>;
41
+ }, z.core.$strip>>>;
42
+ }, z.core.$strip>;
43
+ export type ArtifactStudioManifest = z.infer<typeof artifactStudioManifestSchema>;
44
+ export declare function parseArtifactStudioManifest(value: unknown): ArtifactStudioManifest;
@@ -0,0 +1,45 @@
1
+ import { z } from 'zod';
2
+ import { normalizeArtifactPath } from './paths.js';
3
+ const apiCapabilityPathSchema = z.string().min(1).refine((path) => path.startsWith('/api/') && path !== '/api/' && path !== '/api/*', 'API capability paths must be scoped below /api/');
4
+ const apiCapabilitiesSchema = z.object({
5
+ read: z.array(apiCapabilityPathSchema).default([]),
6
+ write: z.array(apiCapabilityPathSchema).default([]),
7
+ }).default({ read: [], write: [] });
8
+ export const artifactStudioManifestSchema = z.object({
9
+ bundle: z.object({
10
+ name: z.string().min(1).max(255),
11
+ schema_version: z.literal(1).default(1),
12
+ }),
13
+ artifact: z.object({
14
+ project_id: z.string().min(1).max(255).regex(/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/),
15
+ title: z.string().min(1).max(255),
16
+ description: z.string().max(2000).optional(),
17
+ type: z.enum(['react', 'html']).default('react'),
18
+ entrypoint: z.string().min(1).max(512).transform(normalizeArtifactPath),
19
+ }),
20
+ runtime: z.object({
21
+ sdk: z.literal('sequence').default('sequence'),
22
+ react: z.union([z.literal(18), z.literal(19)]).default(18),
23
+ }).default({ sdk: 'sequence', react: 18 }),
24
+ capabilities: z.object({
25
+ data: z.object({
26
+ read: z.array(z.string().min(1)).default([]),
27
+ write: z.array(z.string().min(1)).default([]),
28
+ }).default({ read: [], write: [] }),
29
+ api: apiCapabilitiesSchema,
30
+ functions: z.object({
31
+ invoke: z.array(z.string().min(1)).default([]),
32
+ }).default({ invoke: [] }),
33
+ }).default({
34
+ data: { read: [], write: [] },
35
+ api: { read: [], write: [] },
36
+ functions: { invoke: [] },
37
+ }),
38
+ targets: z.record(z.string().min(1), z.object({
39
+ url: z.string().url(),
40
+ visibility: z.enum(['private', 'shared', 'published']).default('private'),
41
+ })).default({}),
42
+ });
43
+ export function parseArtifactStudioManifest(value) {
44
+ return artifactStudioManifestSchema.parse(value);
45
+ }