@rathnasgala/cli 0.0.21 → 1.0.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 (64) hide show
  1. package/README.md +66 -212
  2. package/package.json +3 -3
  3. package/src/api/gala.js +126 -0
  4. package/src/api/github.js +63 -0
  5. package/src/api/http.js +72 -0
  6. package/src/auth/gala.js +52 -0
  7. package/src/auth/github.js +78 -0
  8. package/src/auth/store.js +88 -0
  9. package/src/cli/args.js +56 -0
  10. package/src/cli/terminal.js +104 -0
  11. package/src/commands/auth.js +20 -0
  12. package/src/commands/doctor.js +98 -0
  13. package/src/commands/init.js +197 -0
  14. package/src/commands/new.js +76 -0
  15. package/src/commands/preview.js +92 -0
  16. package/src/commands/publish.js +57 -0
  17. package/src/commands-manifest.js +58 -0
  18. package/src/content.js +31 -0
  19. package/src/git.js +143 -0
  20. package/src/index.js +44 -297
  21. package/src/publication.js +37 -0
  22. package/src/assign-content-ids.js +0 -1
  23. package/src/auth-command.js +0 -36
  24. package/src/configure-site.js +0 -102
  25. package/src/content-files.js +0 -1
  26. package/src/doctor-command.js +0 -214
  27. package/src/entitlement-client.js +0 -26
  28. package/src/entitlement-command.js +0 -74
  29. package/src/evaluation-date.js +0 -1
  30. package/src/gala-credential-health.js +0 -34
  31. package/src/gala-credential-store.js +0 -115
  32. package/src/gala-device-flow.js +0 -121
  33. package/src/github-auth-command.js +0 -29
  34. package/src/github-credential-store.js +0 -65
  35. package/src/github-device-flow.js +0 -130
  36. package/src/github-empty-repository.js +0 -76
  37. package/src/github-identity.js +0 -32
  38. package/src/github-pages-provisioning.js +0 -107
  39. package/src/github-repository-secret.js +0 -82
  40. package/src/github-repository-variable.js +0 -56
  41. package/src/github-template-repository.js +0 -165
  42. package/src/hook-command.js +0 -64
  43. package/src/http-failure.js +0 -55
  44. package/src/new-command.js +0 -54
  45. package/src/open-browser.js +0 -40
  46. package/src/preview-command.js +0 -60
  47. package/src/publication-creation-client.js +0 -144
  48. package/src/publication-state.js +0 -7
  49. package/src/publish-command.js +0 -37
  50. package/src/record-deployment-command.js +0 -147
  51. package/src/refresh-command.js +0 -104
  52. package/src/repository-limits.js +0 -94
  53. package/src/scaffold-git.js +0 -41
  54. package/src/scaffold-options.js +0 -58
  55. package/src/scaffold-preflight.js +0 -147
  56. package/src/scaffold-site.js +0 -162
  57. package/src/site-config-registration.js +0 -47
  58. package/src/site-registration-client.js +0 -138
  59. package/src/theme-package.js +0 -128
  60. package/src/topology-client.js +0 -43
  61. package/src/topology-command.js +0 -70
  62. package/src/upgrade-command.js +0 -81
  63. package/src/validate-command.js +0 -5
  64. package/src/workflow-command.js +0 -87
@@ -1,81 +0,0 @@
1
- import { lstat, readFile, rm } from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { parse, stringify } from 'yaml';
4
-
5
- import { repairFramework } from './doctor-command.js';
6
- import { fetchVerifiedThemePackage } from './theme-package.js';
7
-
8
- const NAME = '@rathnasgala/theme';
9
- const ACTION_WORKFLOW = '.github/workflows/publish.yml';
10
- const ACTION_REFERENCE = /rathnasgala\/publish\/\.github\/workflows\/publish\.yml@v([1-9][0-9]*)/g;
11
- const ACTION_TAG = /^v([1-9][0-9]*)(?:\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?)?$/;
12
-
13
- async function registryMetadata(fetchImpl) {
14
- const response = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(NAME)}`, {
15
- headers: { Accept: 'application/json' }
16
- });
17
- if (!response.ok) throw new Error(`Theme registry request failed with HTTP ${response.status}`);
18
- return response.json();
19
- }
20
-
21
- export async function inspectActionUpgrade({ root, fetchImpl = fetch }) {
22
- const workflowPath = path.resolve(root, ACTION_WORKFLOW);
23
- const metadata = await lstat(workflowPath);
24
- if (!metadata.isFile() || metadata.isSymbolicLink()) {
25
- throw new TypeError('Publish workflow must be a regular file');
26
- }
27
- const workflow = await readFile(workflowPath, 'utf8');
28
- const majors = [...workflow.matchAll(ACTION_REFERENCE)].map((match) => Number(match[1]));
29
- if (majors.length === 0 || new Set(majors).size !== 1) {
30
- throw new Error('Publish workflow must reference exactly one Gala action major');
31
- }
32
- const response = await fetchImpl('https://api.github.com/repos/rathnasgala/publish/tags?per_page=100', {
33
- headers: { Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2026-03-10' }
34
- });
35
- if (!response.ok) throw new Error(`Action release lookup failed with HTTP ${response.status}`);
36
- const tags = await response.json();
37
- if (!Array.isArray(tags)) throw new TypeError('Action release response must be an array');
38
- const releasedMajors = tags.map((tag) => ACTION_TAG.exec(tag?.name)?.[1])
39
- .filter((major) => major != null).map(Number);
40
- const currentMajor = majors[0];
41
- const latestMajor = releasedMajors.length === 0 ? currentMajor : Math.max(...releasedMajors);
42
- return Object.freeze({ currentMajor, latestMajor, newerAvailable: latestMajor > currentMajor });
43
- }
44
-
45
- export async function upgradeTheme({ root, channel, confirm, fetchImpl = fetch }) {
46
- const configPath = path.resolve(root, 'site.config.yml');
47
- const config = parse(await readFile(configPath, 'utf8'));
48
- if (config?.canonicalPolicy != null) {
49
- if (config.canonicalPolicy !== 'self' || config.hosting == null || Array.isArray(config.hosting)
50
- || (config.hosting.canonicalPolicy != null && config.hosting.canonicalPolicy !== 'self')) {
51
- throw new TypeError('Legacy canonicalPolicy cannot be migrated safely');
52
- }
53
- config.hosting.canonicalPolicy = 'self';
54
- delete config.canonicalPolicy;
55
- }
56
- const installed = config?.framework?.themePackage?.version;
57
- const [metadata, action] = await Promise.all([
58
- registryMetadata(fetchImpl), inspectActionUpgrade({ root, fetchImpl })
59
- ]);
60
- const selectedChannel = channel ?? (metadata['dist-tags']?.next === installed ? 'next' : 'latest');
61
- if (!['latest', 'next'].includes(selectedChannel)) throw new TypeError('theme channel must be latest or next');
62
- const version = metadata['dist-tags']?.[selectedChannel];
63
- if (typeof version !== 'string') throw new Error(`Theme channel ${selectedChannel} has no resolved version`);
64
- if (version === installed) return { changed: false, channel: selectedChannel, version, repaired: [], action };
65
- if (typeof confirm !== 'function' || !await confirm({ name: NAME, installed, channel: selectedChannel, version })) {
66
- return { changed: false, cancelled: true, channel: selectedChannel, version, repaired: [], action };
67
- }
68
- const downloaded = await fetchVerifiedThemePackage({ name: NAME, version, fetchImpl });
69
- try {
70
- if (!downloaded.manifest.themePackage.availableDesignThemes?.includes(config?.design?.theme)) {
71
- throw new Error(`Visual theme ${String(config?.design?.theme)} is unavailable in ${NAME}@${version}`);
72
- }
73
- config.framework.themePackage = { name: NAME, version };
74
- const repaired = await repairFramework(root, downloaded.staging, {
75
- siteConfiguration: stringify(config)
76
- });
77
- return { changed: true, channel: selectedChannel, version, repaired, action };
78
- } finally {
79
- await rm(downloaded.cleanupRoot, { recursive: true, force: true });
80
- }
81
- }
@@ -1,5 +0,0 @@
1
- export {
2
- BUILD_MANIFEST_PATH,
3
- regenerateBuildManifest,
4
- validateContent
5
- } from '@rathnasgala/content-validation';
@@ -1,87 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
- import path from 'node:path';
4
-
5
- const ACTION_REF = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/.github\/workflows\/[A-Za-z0-9_.-]+\.ya?ml@v(?:[1-9][0-9]*|[0-9]+\.[0-9]+\.[0-9]+)$/;
6
- const BRANCH = /^(?![./])(?!.*\.\.)(?!.*[~^:?*\[\\])[A-Za-z0-9._/-]+(?<![/.])$/;
7
-
8
- export function deriveNightlySchedule(siteId) {
9
- if (typeof siteId !== 'string' || siteId.trim() === '') throw new TypeError('siteId is required');
10
- const digest = createHash('sha256').update(siteId, 'utf8').digest();
11
- return { minute: digest.readUInt16BE(0) % 60, hour: digest.readUInt16BE(2) % 24 };
12
- }
13
-
14
- function validateTimezone(timezone) {
15
- try {
16
- new Intl.DateTimeFormat('en-US', { timeZone: timezone }).format(0);
17
- } catch {
18
- throw new TypeError(`Invalid IANA timezone: ${timezone}`);
19
- }
20
- }
21
-
22
- export async function writePublishWorkflow({
23
- root,
24
- siteId,
25
- timezone,
26
- actionRef = 'rathnasgala/publish/.github/workflows/publish.yml@v1',
27
- defaultBranch = 'main',
28
- buildMode = 'build-and-deploy'
29
- }) {
30
- validateTimezone(timezone);
31
- if (!ACTION_REF.test(actionRef)) {
32
- throw new TypeError('actionRef must pin a reusable workflow to a major or immutable semver tag');
33
- }
34
- if (!BRANCH.test(defaultBranch)) throw new TypeError('Invalid default branch');
35
- if (!['build-only', 'build-and-deploy'].includes(buildMode)) throw new TypeError('Invalid build mode');
36
-
37
- const resolvedRoot = path.resolve(root);
38
- const templatePath = path.join(resolvedRoot, '.gala', 'publish.yml.template');
39
- const target = path.join(resolvedRoot, '.github', 'workflows', 'publish.yml');
40
- const templateMetadata = await lstat(templatePath);
41
- if (!templateMetadata.isFile() || templateMetadata.isSymbolicLink()) {
42
- throw new TypeError('Workflow template must be a regular file');
43
- }
44
- try {
45
- const targetMetadata = await lstat(target);
46
- if (targetMetadata.isSymbolicLink() || !targetMetadata.isFile()) {
47
- throw new TypeError('Workflow target must be a regular file');
48
- }
49
- } catch (error) {
50
- if (error.code !== 'ENOENT') throw error;
51
- }
52
-
53
- const { minute, hour } = deriveNightlySchedule(siteId);
54
- const workflow = (await readFile(templatePath, 'utf8'))
55
- .replaceAll('__DEFAULT_BRANCH__', defaultBranch)
56
- .replaceAll('__CRON__', `${minute} ${hour} * * *`)
57
- .replaceAll('__TIMEZONE__', timezone)
58
- .replaceAll('__SITE_ID__', siteId)
59
- .replaceAll('__ACTION_REF__', actionRef)
60
- .replaceAll('__BUILD_MODE__', buildMode);
61
- if (/__[A-Z_]+__/.test(workflow)) throw new TypeError('Workflow template contains unresolved placeholders');
62
-
63
- await mkdir(path.dirname(target), { recursive: true });
64
- const temporary = `${target}.gala-workflow-${process.pid}`;
65
- const backup = `${target}.gala-backup-${process.pid}`;
66
- let backedUp = false;
67
- try {
68
- await writeFile(temporary, workflow, { flag: 'wx' });
69
- try {
70
- await rename(target, backup);
71
- backedUp = true;
72
- } catch (error) {
73
- if (error.code !== 'ENOENT') throw error;
74
- }
75
- try {
76
- await rename(temporary, target);
77
- } catch (error) {
78
- if (backedUp) await rename(backup, target);
79
- throw error;
80
- }
81
- if (backedUp) await rm(backup);
82
- } catch (error) {
83
- await rm(temporary, { force: true });
84
- throw error;
85
- }
86
- return { target, minute, hour };
87
- }