@smoothbricks/cli 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 (86) hide show
  1. package/README.md +289 -0
  2. package/bin/smoo +4 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +141 -0
  6. package/dist/github-ci/index.d.ts +11 -0
  7. package/dist/github-ci/index.d.ts.map +1 -0
  8. package/dist/github-ci/index.js +174 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/lib/json.d.ts +13 -0
  13. package/dist/lib/json.d.ts.map +1 -0
  14. package/dist/lib/json.js +57 -0
  15. package/dist/lib/run.d.ts +5 -0
  16. package/dist/lib/run.d.ts.map +1 -0
  17. package/dist/lib/run.js +64 -0
  18. package/dist/lib/workspace.d.ts +23 -0
  19. package/dist/lib/workspace.d.ts.map +1 -0
  20. package/dist/lib/workspace.js +101 -0
  21. package/dist/monorepo/commit-msg.d.ts +2 -0
  22. package/dist/monorepo/commit-msg.d.ts.map +1 -0
  23. package/dist/monorepo/commit-msg.js +34 -0
  24. package/dist/monorepo/git-config.d.ts +2 -0
  25. package/dist/monorepo/git-config.d.ts.map +1 -0
  26. package/dist/monorepo/git-config.js +37 -0
  27. package/dist/monorepo/index.d.ts +19 -0
  28. package/dist/monorepo/index.d.ts.map +1 -0
  29. package/dist/monorepo/index.js +59 -0
  30. package/dist/monorepo/lockfile.d.ts +2 -0
  31. package/dist/monorepo/lockfile.d.ts.map +1 -0
  32. package/dist/monorepo/lockfile.js +34 -0
  33. package/dist/monorepo/managed-files.d.ts +8 -0
  34. package/dist/monorepo/managed-files.d.ts.map +1 -0
  35. package/dist/monorepo/managed-files.js +130 -0
  36. package/dist/monorepo/nx-sync.d.ts +2 -0
  37. package/dist/monorepo/nx-sync.d.ts.map +1 -0
  38. package/dist/monorepo/nx-sync.js +5 -0
  39. package/dist/monorepo/package-hygiene.d.ts +3 -0
  40. package/dist/monorepo/package-hygiene.d.ts.map +1 -0
  41. package/dist/monorepo/package-hygiene.js +12 -0
  42. package/dist/monorepo/package-policy.d.ts +8 -0
  43. package/dist/monorepo/package-policy.d.ts.map +1 -0
  44. package/dist/monorepo/package-policy.js +279 -0
  45. package/dist/monorepo/packed-package.d.ts +2 -0
  46. package/dist/monorepo/packed-package.d.ts.map +1 -0
  47. package/dist/monorepo/packed-package.js +92 -0
  48. package/dist/monorepo/packs/index.d.ts +10 -0
  49. package/dist/monorepo/packs/index.d.ts.map +1 -0
  50. package/dist/monorepo/packs/index.js +87 -0
  51. package/dist/monorepo/runtime.d.ts +2 -0
  52. package/dist/monorepo/runtime.d.ts.map +1 -0
  53. package/dist/monorepo/runtime.js +37 -0
  54. package/dist/release/index.d.ts +24 -0
  55. package/dist/release/index.d.ts.map +1 -0
  56. package/dist/release/index.js +325 -0
  57. package/managed/raw/.git-format-staged.yml +47 -0
  58. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +66 -0
  59. package/managed/raw/tooling/git-hooks/commit-msg.sh +9 -0
  60. package/managed/raw/tooling/git-hooks/pre-commit.sh +19 -0
  61. package/managed/templates/github/actions/cache-nix-devenv/action.yml +79 -0
  62. package/managed/templates/github/actions/cache-node-modules/action.yml +13 -0
  63. package/managed/templates/github/actions/cache-nx/action.yml +21 -0
  64. package/managed/templates/github/actions/save-nix-devenv/action.yml +39 -0
  65. package/managed/templates/github/actions/setup-devenv/action.yml +63 -0
  66. package/managed/templates/github/workflows/ci.yml +98 -0
  67. package/managed/templates/github/workflows/publish.yml +136 -0
  68. package/package.json +68 -0
  69. package/src/cli.ts +149 -0
  70. package/src/github-ci/index.ts +196 -0
  71. package/src/index.ts +1 -0
  72. package/src/lib/json.ts +68 -0
  73. package/src/lib/run.ts +76 -0
  74. package/src/lib/workspace.ts +125 -0
  75. package/src/monorepo/commit-msg.ts +35 -0
  76. package/src/monorepo/git-config.ts +43 -0
  77. package/src/monorepo/index.ts +78 -0
  78. package/src/monorepo/lockfile.ts +37 -0
  79. package/src/monorepo/managed-files.ts +153 -0
  80. package/src/monorepo/nx-sync.ts +6 -0
  81. package/src/monorepo/package-hygiene.ts +14 -0
  82. package/src/monorepo/package-policy.ts +307 -0
  83. package/src/monorepo/packed-package.ts +101 -0
  84. package/src/monorepo/packs/index.ts +115 -0
  85. package/src/monorepo/runtime.ts +41 -0
  86. package/src/release/index.ts +394 -0
@@ -0,0 +1,57 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { hasOwn, hasOwnString, isRecord } from '@smoothbricks/validation';
3
+ export { hasOwn, hasOwnString, isRecord };
4
+ export function stringProperty(record, key) {
5
+ const value = record[key];
6
+ return typeof value === 'string' && value.length > 0 ? value : null;
7
+ }
8
+ export function recordProperty(record, key) {
9
+ const value = record[key];
10
+ return isRecord(value) ? value : null;
11
+ }
12
+ export function getOrCreateRecord(record, key) {
13
+ const value = record[key];
14
+ if (isRecord(value)) {
15
+ return value;
16
+ }
17
+ const next = {};
18
+ record[key] = next;
19
+ return next;
20
+ }
21
+ export function setStringProperty(record, key, value) {
22
+ if (record[key] === value) {
23
+ return false;
24
+ }
25
+ record[key] = value;
26
+ return true;
27
+ }
28
+ export function setMissingStringProperty(record, key, value) {
29
+ if (typeof record[key] === 'string') {
30
+ return false;
31
+ }
32
+ record[key] = value;
33
+ return true;
34
+ }
35
+ export function requiredJsonObject(path) {
36
+ const json = readJsonObject(path);
37
+ if (!json) {
38
+ throw new Error(`${path} not found or invalid`);
39
+ }
40
+ return json;
41
+ }
42
+ export function readJsonObject(path) {
43
+ const json = readJson(path);
44
+ return isRecord(json) ? json : null;
45
+ }
46
+ export function writeJsonObject(path, value) {
47
+ writeFileSync(path, jsonObjectText(value));
48
+ }
49
+ export function jsonObjectText(value) {
50
+ return `${JSON.stringify(value, null, 2)}\n`;
51
+ }
52
+ export function readJson(path) {
53
+ if (!existsSync(path)) {
54
+ return null;
55
+ }
56
+ return JSON.parse(readFileSync(path, 'utf8'));
57
+ }
@@ -0,0 +1,5 @@
1
+ export declare function run(command: string, args: string[], cwd: string, env?: Record<string, string>): Promise<void>;
2
+ export declare function runStatus(command: string, args: string[], cwd: string, quiet?: boolean, env?: Record<string, string>): Promise<number>;
3
+ export declare function findRepoRoot(): Promise<string>;
4
+ export declare function decode(bytes: Uint8Array): string;
5
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/lib/run.ts"],"names":[],"mappings":"AAKA,wBAAsB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnH;AAED,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,EACX,KAAK,UAAQ,EACb,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC3B,OAAO,CAAC,MAAM,CAAC,CAQjB;AAuCD,wBAAsB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAMpD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEhD"}
@@ -0,0 +1,64 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { $ } from 'bun';
5
+ export async function run(command, args, cwd, env) {
6
+ const status = await runStatus(command, args, cwd, false, env);
7
+ if (status !== 0) {
8
+ throw new Error(`${command} ${args.join(' ')} failed with exit code ${status}`);
9
+ }
10
+ }
11
+ export async function runStatus(command, args, cwd, quiet = false, env) {
12
+ const invocation = resolveCommandInvocation(cwd, command, args);
13
+ let shell = $ `${invocation.command} ${invocation.args}`.cwd(cwd).nothrow();
14
+ if (env) {
15
+ shell = shell.env(mergeEnv(env));
16
+ }
17
+ const result = quiet ? await shell.quiet() : await shell;
18
+ return result.exitCode;
19
+ }
20
+ function mergeEnv(env) {
21
+ const merged = {};
22
+ for (const [key, value] of Object.entries(process.env)) {
23
+ if (value !== undefined) {
24
+ merged[key] = value;
25
+ }
26
+ }
27
+ return { ...merged, ...env };
28
+ }
29
+ function resolveCommandInvocation(root, command, args) {
30
+ const localCommand = join(root, 'node_modules', '.bin', command);
31
+ if (existsSync(localCommand)) {
32
+ return { command: localCommand, args };
33
+ }
34
+ const bundledCommand = resolveBundledCommand(command);
35
+ if (bundledCommand) {
36
+ return { command: 'bun', args: [bundledCommand, ...args] };
37
+ }
38
+ return { command, args };
39
+ }
40
+ function resolveBundledCommand(command) {
41
+ try {
42
+ if (command === 'sherif') {
43
+ return fileURLToPath(import.meta.resolve('sherif'));
44
+ }
45
+ if (command === 'attw') {
46
+ const packageJson = fileURLToPath(import.meta.resolve('@arethetypeswrong/cli/package.json'));
47
+ return join(dirname(packageJson), 'dist', 'index.js');
48
+ }
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ return null;
54
+ }
55
+ export async function findRepoRoot() {
56
+ const result = await $ `git rev-parse --show-toplevel`.cwd(process.cwd()).quiet().nothrow();
57
+ if (result.exitCode === 0) {
58
+ return decode(result.stdout).trim();
59
+ }
60
+ return process.cwd();
61
+ }
62
+ export function decode(bytes) {
63
+ return new TextDecoder().decode(bytes);
64
+ }
@@ -0,0 +1,23 @@
1
+ export interface PackageInfo {
2
+ name: string;
3
+ version: string;
4
+ private: boolean;
5
+ tags: string[];
6
+ path: string;
7
+ packageJsonPath: string;
8
+ json: Record<string, unknown>;
9
+ }
10
+ export interface RepositoryInfo {
11
+ type: string;
12
+ url: string;
13
+ }
14
+ export declare const workspaceDependencyFields: readonly ["dependencies", "devDependencies", "optionalDependencies"];
15
+ export declare function listPublicPackages(root: string): PackageInfo[];
16
+ export declare function getWorkspacePackages(root: string): PackageInfo[];
17
+ export declare function listPackageJsonRecords(root: string): PackageInfo[];
18
+ export declare function getWorkspacePatterns(root: string): string[];
19
+ export declare function readPackageJson(path: string): PackageInfo | null;
20
+ export declare function getNxTags(pkg: Record<string, unknown>): string[];
21
+ export declare function repositoryInfo(pkg: Record<string, unknown>): RepositoryInfo | null;
22
+ export declare function escapeRegex(value: string): string;
23
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../src/lib/workspace.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,eAAO,MAAM,yBAAyB,sEAAuE,CAAC;AAE9G,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAE9D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAgChE;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,CAMlE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAa3D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAgBhE;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAKhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,GAAG,IAAI,CAalF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD"}
@@ -0,0 +1,101 @@
1
+ import { readdirSync, statSync } from 'node:fs';
2
+ import { dirname, join, relative } from 'node:path';
3
+ import { hasOwn, hasOwnString, isRecord, readJson, readJsonObject, stringProperty } from './json.js';
4
+ export const workspaceDependencyFields = ['dependencies', 'devDependencies', 'optionalDependencies'];
5
+ export function listPublicPackages(root) {
6
+ return getWorkspacePackages(root).filter((pkg) => !pkg.private && pkg.tags.includes('npm:public'));
7
+ }
8
+ export function getWorkspacePackages(root) {
9
+ if (!readPackageJson(join(root, 'package.json'))) {
10
+ throw new Error('package.json not found or invalid');
11
+ }
12
+ const workspacePatterns = getWorkspacePatterns(root);
13
+ const packages = [];
14
+ for (const pattern of workspacePatterns) {
15
+ if (!pattern.endsWith('/*')) {
16
+ continue;
17
+ }
18
+ const parent = join(root, pattern.slice(0, -2));
19
+ if (!statSync(parent, { throwIfNoEntry: false })?.isDirectory()) {
20
+ continue;
21
+ }
22
+ for (const entry of readdirSync(parent)) {
23
+ const pkgPath = join(parent, entry, 'package.json');
24
+ const pkg = readPackageJson(pkgPath);
25
+ if (!pkg?.name || !pkg.version) {
26
+ continue;
27
+ }
28
+ packages.push({
29
+ name: pkg.name,
30
+ version: pkg.version,
31
+ private: pkg.private,
32
+ tags: pkg.tags,
33
+ path: relative(root, dirname(pkgPath)),
34
+ packageJsonPath: pkg.packageJsonPath,
35
+ json: pkg.json,
36
+ });
37
+ }
38
+ }
39
+ return packages.sort((a, b) => a.name.localeCompare(b.name));
40
+ }
41
+ export function listPackageJsonRecords(root) {
42
+ const rootPackage = readPackageJson(join(root, 'package.json'));
43
+ if (!rootPackage) {
44
+ throw new Error('package.json not found or invalid');
45
+ }
46
+ return [{ ...rootPackage, path: '.' }, ...getWorkspacePackages(root)];
47
+ }
48
+ export function getWorkspacePatterns(root) {
49
+ const raw = readJson(join(root, 'package.json'));
50
+ if (!isRecord(raw) || !hasOwn(raw, 'workspaces')) {
51
+ return ['packages/*'];
52
+ }
53
+ const workspaces = raw.workspaces;
54
+ if (Array.isArray(workspaces)) {
55
+ return workspaces.filter((entry) => typeof entry === 'string');
56
+ }
57
+ if (isRecord(workspaces) && hasOwn(workspaces, 'packages') && Array.isArray(workspaces.packages)) {
58
+ return workspaces.packages.filter((entry) => typeof entry === 'string');
59
+ }
60
+ return ['packages/*'];
61
+ }
62
+ export function readPackageJson(path) {
63
+ const parsed = readJsonObject(path);
64
+ if (!isRecord(parsed) || !hasOwnString(parsed, 'name') || !hasOwnString(parsed, 'version')) {
65
+ return null;
66
+ }
67
+ const privateValue = hasOwn(parsed, 'private') && typeof parsed.private === 'boolean' ? parsed.private : false;
68
+ const tags = getNxTags(parsed);
69
+ return {
70
+ name: parsed.name,
71
+ version: parsed.version,
72
+ private: privateValue,
73
+ tags,
74
+ path: dirname(path),
75
+ packageJsonPath: path,
76
+ json: parsed,
77
+ };
78
+ }
79
+ export function getNxTags(pkg) {
80
+ if (!hasOwn(pkg, 'nx') || !isRecord(pkg.nx) || !hasOwn(pkg.nx, 'tags') || !Array.isArray(pkg.nx.tags)) {
81
+ return [];
82
+ }
83
+ return pkg.nx.tags.filter((tag) => typeof tag === 'string');
84
+ }
85
+ export function repositoryInfo(pkg) {
86
+ const repository = pkg.repository;
87
+ if (typeof repository === 'string') {
88
+ return { type: 'git', url: repository };
89
+ }
90
+ if (!isRecord(repository)) {
91
+ return null;
92
+ }
93
+ const url = stringProperty(repository, 'url');
94
+ if (!url) {
95
+ return null;
96
+ }
97
+ return { type: stringProperty(repository, 'type') ?? 'git', url };
98
+ }
99
+ export function escapeRegex(value) {
100
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
101
+ }
@@ -0,0 +1,2 @@
1
+ export declare function validateCommitMessage(message: string): string | null;
2
+ //# sourceMappingURL=commit-msg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-msg.d.ts","sourceRoot":"","sources":["../../src/monorepo/commit-msg.ts"],"names":[],"mappings":"AAcA,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAoBpE"}
@@ -0,0 +1,34 @@
1
+ const validCommitTypes = new Set([
2
+ 'build',
3
+ 'chore',
4
+ 'ci',
5
+ 'docs',
6
+ 'feat',
7
+ 'fix',
8
+ 'perf',
9
+ 'refactor',
10
+ 'revert',
11
+ 'style',
12
+ 'test',
13
+ ]);
14
+ export function validateCommitMessage(message) {
15
+ const subject = message.split('\n', 1)[0]?.trim() ?? '';
16
+ if (!subject) {
17
+ return 'Commit message subject is empty.';
18
+ }
19
+ if (/^(Merge|Revert ")/.test(subject) || /^(fixup|squash)! /.test(subject)) {
20
+ return null;
21
+ }
22
+ const match = /^(?<type>[a-z]+)(\([a-z0-9._/-]+\))?(?<breaking>!)?: .+$/.exec(subject);
23
+ const type = match?.groups?.type;
24
+ if (type && validCommitTypes.has(type)) {
25
+ return null;
26
+ }
27
+ return `Invalid conventional commit subject: ${subject}
28
+
29
+ Expected examples:
30
+ feat(statebus-core): add optimistic transactions
31
+ fix(money): round negative amounts consistently
32
+ chore(release): publish 1.2.3
33
+ feat!: remove deprecated API`;
34
+ }
@@ -0,0 +1,2 @@
1
+ export declare function applyWorkspaceGitConfig(root: string): Promise<void>;
2
+ //# sourceMappingURL=git-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-config.d.ts","sourceRoot":"","sources":["../../src/monorepo/git-config.ts"],"names":[],"mappings":"AAKA,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAYzE"}
@@ -0,0 +1,37 @@
1
+ import { existsSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { $ } from 'bun';
4
+ import { decode } from '../lib/run.js';
5
+ export async function applyWorkspaceGitConfig(root) {
6
+ const gitDirResult = await $ `git rev-parse --git-dir`.cwd(root).quiet().nothrow();
7
+ if (gitDirResult.exitCode !== 0) {
8
+ throw new Error('Not in a git repository');
9
+ }
10
+ const gitDir = resolve(root, decode(gitDirResult.stdout).trim());
11
+ const tooling = join(root, 'tooling');
12
+ await $ `git config --local include.path ${join(tooling, 'workspace.gitconfig')}`.cwd(root);
13
+ linkHook(gitDir, tooling, 'pre-commit');
14
+ linkHook(gitDir, tooling, 'commit-msg');
15
+ }
16
+ function linkHook(gitDir, tooling, name) {
17
+ const source = join(tooling, 'git-hooks', `${name}.sh`);
18
+ if (!existsSync(source)) {
19
+ throw new Error(`Missing ${name} hook source: ${source}`);
20
+ }
21
+ const target = join(gitDir, 'hooks', name);
22
+ if (readLinkOrNull(target) === source) {
23
+ return;
24
+ }
25
+ console.log(`[!] Linking ${name} hook in ${gitDir}`);
26
+ mkdirSync(dirname(target), { recursive: true });
27
+ rmSync(target, { force: true });
28
+ symlinkSync(source, target);
29
+ }
30
+ function readLinkOrNull(path) {
31
+ try {
32
+ return readlinkSync(path);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
@@ -0,0 +1,19 @@
1
+ import { applyWorkspaceGitConfig } from './git-config.js';
2
+ import { syncBunLockfileVersions } from './lockfile.js';
3
+ export interface InitOptions {
4
+ runtimeOnly?: boolean;
5
+ syncRuntime?: boolean;
6
+ }
7
+ export interface ValidateOptions {
8
+ failFast?: boolean;
9
+ }
10
+ export declare function initMonorepo(root: string, options: InitOptions): Promise<void>;
11
+ export declare function validateMonorepo(root: string, options?: ValidateOptions): Promise<void>;
12
+ export declare function updateManagedFiles(root: string): void;
13
+ export declare function checkManagedFiles(root: string): void;
14
+ export declare function diffManagedFiles(root: string): void;
15
+ export declare function validateCommitMessageFile(path: string | undefined): void;
16
+ export declare function listPublicProjects(root: string): string;
17
+ export declare function validatePublicPackageTags(root: string): void;
18
+ export { applyWorkspaceGitConfig, syncBunLockfileVersions };
19
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/monorepo/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAMxD,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAQpF;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOjG;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAErD;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMpD;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CASxE;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIvD;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI5D;AAED,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { listPublicPackages } from '../lib/workspace.js';
3
+ import { validateCommitMessage } from './commit-msg.js';
4
+ import { applyWorkspaceGitConfig } from './git-config.js';
5
+ import { syncBunLockfileVersions } from './lockfile.js';
6
+ import { applyManagedFiles, printResults } from './managed-files.js';
7
+ import { validatePublicTags } from './package-policy.js';
8
+ import { runInitPacks, runValidatePacks } from './packs/index.js';
9
+ import { syncRootRuntimeVersions } from './runtime.js';
10
+ export async function initMonorepo(root, options) {
11
+ if (options.runtimeOnly) {
12
+ await syncRootRuntimeVersions(root);
13
+ return;
14
+ }
15
+ printResults(applyManagedFiles(root, 'update'));
16
+ await runInitPacks({ root, syncRuntime: process.env.DEVENV_ROOT !== undefined || options.syncRuntime === true });
17
+ }
18
+ export async function validateMonorepo(root, options = {}) {
19
+ const failures = await runValidatePacks({ root, syncRuntime: false }, options);
20
+ if (failures > 0) {
21
+ throw new Error(`Monorepo validation failed with ${failures} problem(s).`);
22
+ }
23
+ console.log('\n== summary ==');
24
+ console.log('Monorepo configuration is valid.');
25
+ }
26
+ export function updateManagedFiles(root) {
27
+ printResults(applyManagedFiles(root, 'update'));
28
+ }
29
+ export function checkManagedFiles(root) {
30
+ const results = applyManagedFiles(root, 'check');
31
+ printResults(results);
32
+ if (results.some((result) => result.action === 'drifted')) {
33
+ throw new Error('Managed monorepo files are out of date. Run: smoo monorepo update');
34
+ }
35
+ }
36
+ export function diffManagedFiles(root) {
37
+ printResults(applyManagedFiles(root, 'diff'));
38
+ }
39
+ export function validateCommitMessageFile(path) {
40
+ if (!path) {
41
+ throw new Error('Usage: smoo monorepo validate-commit-msg <commit-msg-file>');
42
+ }
43
+ const message = readFileSync(path, 'utf8');
44
+ const error = validateCommitMessage(message);
45
+ if (error) {
46
+ throw new Error(error);
47
+ }
48
+ }
49
+ export function listPublicProjects(root) {
50
+ return listPublicPackages(root)
51
+ .map((pkg) => pkg.name)
52
+ .join(',');
53
+ }
54
+ export function validatePublicPackageTags(root) {
55
+ if (validatePublicTags(root) > 0) {
56
+ throw new Error('npm:public tag validation failed.');
57
+ }
58
+ }
59
+ export { applyWorkspaceGitConfig, syncBunLockfileVersions };
@@ -0,0 +1,2 @@
1
+ export declare function syncBunLockfileVersions(root: string): void;
2
+ //# sourceMappingURL=lockfile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lockfile.d.ts","sourceRoot":"","sources":["../../src/monorepo/lockfile.ts"],"names":[],"mappings":"AAIA,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAgC1D"}
@@ -0,0 +1,34 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { escapeRegex, getWorkspacePackages } from '../lib/workspace.js';
4
+ export function syncBunLockfileVersions(root) {
5
+ const lockfilePath = join(root, 'bun.lock');
6
+ if (!existsSync(lockfilePath)) {
7
+ throw new Error('bun.lock not found');
8
+ }
9
+ const packages = getWorkspacePackages(root);
10
+ let lockfile = readFileSync(lockfilePath, 'utf8');
11
+ let updated = 0;
12
+ for (const pkg of packages) {
13
+ const relativePath = pkg.path.replaceAll('\\', '/');
14
+ const escaped = escapeRegex(relativePath);
15
+ const pattern = new RegExp(`("${escaped}":\\s*\\{[^}]*"version":\\s*")([^"]+)(")`);
16
+ const match = lockfile.match(pattern);
17
+ if (!match) {
18
+ console.log(`skip: ${relativePath} (not found in lockfile)`);
19
+ continue;
20
+ }
21
+ const lockVersion = match[2];
22
+ if (lockVersion === pkg.version) {
23
+ console.log(`ok: ${relativePath} = ${pkg.version}`);
24
+ continue;
25
+ }
26
+ lockfile = lockfile.replace(pattern, `$1${pkg.version}$3`);
27
+ console.log(`fix: ${relativePath}: ${lockVersion} -> ${pkg.version}`);
28
+ updated++;
29
+ }
30
+ if (updated > 0) {
31
+ writeFileSync(lockfilePath, lockfile);
32
+ }
33
+ console.log(updated > 0 ? `Updated ${updated} workspace version(s) in bun.lock` : 'All workspace versions already in sync.');
34
+ }
@@ -0,0 +1,8 @@
1
+ export interface FileResult {
2
+ target: string;
3
+ action: 'created' | 'updated' | 'unchanged' | 'skipped-symlink' | 'drifted' | 'ok-symlink';
4
+ }
5
+ export declare function applyManagedFiles(root: string, mode: 'update' | 'check' | 'diff'): FileResult[];
6
+ export declare function printResults(results: FileResult[]): void;
7
+ export declare function validateManagedFiles(root: string): number;
8
+ //# sourceMappingURL=managed-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managed-files.d.ts","sourceRoot":"","sources":["../../src/monorepo/managed-files.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,iBAAiB,GAAG,SAAS,GAAG,YAAY,CAAC;CAC5F;AAiED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,EAAE,CAE/F;AAsDD,wBAAgB,YAAY,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,IAAI,CAIxD;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQzD"}
@@ -0,0 +1,130 @@
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { readPackageJson } from '../lib/workspace.js';
5
+ const managedFiles = [
6
+ {
7
+ kind: 'raw',
8
+ source: 'tooling/direnv/github-actions-bootstrap.sh',
9
+ target: 'tooling/direnv/github-actions-bootstrap.sh',
10
+ executable: true,
11
+ },
12
+ {
13
+ kind: 'raw',
14
+ source: 'tooling/git-hooks/pre-commit.sh',
15
+ target: 'tooling/git-hooks/pre-commit.sh',
16
+ executable: true,
17
+ },
18
+ {
19
+ kind: 'raw',
20
+ source: 'tooling/git-hooks/commit-msg.sh',
21
+ target: 'tooling/git-hooks/commit-msg.sh',
22
+ executable: true,
23
+ },
24
+ {
25
+ kind: 'raw',
26
+ source: '.git-format-staged.yml',
27
+ target: '.git-format-staged.yml',
28
+ },
29
+ {
30
+ kind: 'template',
31
+ source: 'github/workflows/ci.yml',
32
+ target: '.github/workflows/ci.yml',
33
+ },
34
+ {
35
+ kind: 'template',
36
+ source: 'github/workflows/publish.yml',
37
+ target: '.github/workflows/publish.yml',
38
+ },
39
+ {
40
+ kind: 'template',
41
+ source: 'github/actions/cache-nix-devenv/action.yml',
42
+ target: '.github/actions/cache-nix-devenv/action.yml',
43
+ },
44
+ {
45
+ kind: 'template',
46
+ source: 'github/actions/setup-devenv/action.yml',
47
+ target: '.github/actions/setup-devenv/action.yml',
48
+ },
49
+ {
50
+ kind: 'template',
51
+ source: 'github/actions/save-nix-devenv/action.yml',
52
+ target: '.github/actions/save-nix-devenv/action.yml',
53
+ },
54
+ {
55
+ kind: 'template',
56
+ source: 'github/actions/cache-node-modules/action.yml',
57
+ target: '.github/actions/cache-node-modules/action.yml',
58
+ },
59
+ {
60
+ kind: 'template',
61
+ source: 'github/actions/cache-nx/action.yml',
62
+ target: '.github/actions/cache-nx/action.yml',
63
+ },
64
+ ];
65
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
66
+ export function applyManagedFiles(root, mode) {
67
+ return managedFiles.map((file) => applyManagedFile(root, file, mode));
68
+ }
69
+ function applyManagedFile(root, file, mode) {
70
+ const target = resolve(root, file.target);
71
+ const content = getManagedContent(root, file);
72
+ if (existsSync(target)) {
73
+ const info = lstatSync(target);
74
+ if (info.isSymbolicLink()) {
75
+ return { target: file.target, action: mode === 'check' ? 'ok-symlink' : 'skipped-symlink' };
76
+ }
77
+ if (!info.isFile()) {
78
+ throw new Error(`${file.target} exists but is not a regular file or symlink`);
79
+ }
80
+ const current = readFileSync(target, 'utf8');
81
+ if (current === content) {
82
+ return { target: file.target, action: 'unchanged' };
83
+ }
84
+ if (mode === 'check' || mode === 'diff') {
85
+ return { target: file.target, action: 'drifted' };
86
+ }
87
+ writeManagedFile(target, content, file.executable === true);
88
+ return { target: file.target, action: 'updated' };
89
+ }
90
+ if (mode === 'check' || mode === 'diff') {
91
+ return { target: file.target, action: 'drifted' };
92
+ }
93
+ writeManagedFile(target, content, file.executable === true);
94
+ return { target: file.target, action: 'created' };
95
+ }
96
+ function getManagedContent(root, file) {
97
+ const sourceRoot = file.kind === 'raw' ? 'managed/raw' : 'managed/templates';
98
+ const sourcePath = join(packageRoot, sourceRoot, file.source);
99
+ const content = readFileSync(sourcePath, 'utf8');
100
+ if (file.kind === 'raw') {
101
+ return content;
102
+ }
103
+ return renderTemplate(root, content);
104
+ }
105
+ function renderTemplate(root, template) {
106
+ const packageJson = readPackageJson(join(root, 'package.json'));
107
+ const repoName = packageJson?.name ?? 'monorepo';
108
+ const nodeModulesCacheKey = existsSync(join(root, 'bun.lock'))
109
+ ? `$${"{{ hashFiles('bun.lock', 'package.json', 'packages/*/package.json') }}"}`
110
+ : `$${"{{ hashFiles('bun.lockb', 'package.json', 'packages/*/package.json') }}"}`;
111
+ return template.replaceAll('{{REPO_NAME}}', repoName).replaceAll('{{NODE_MODULES_CACHE_KEY}}', nodeModulesCacheKey);
112
+ }
113
+ function writeManagedFile(path, content, executable) {
114
+ mkdirSync(dirname(path), { recursive: true });
115
+ writeFileSync(path, content, { mode: executable ? 0o755 : 0o644 });
116
+ }
117
+ export function printResults(results) {
118
+ for (const result of results) {
119
+ console.log(`${result.action.padEnd(15)} ${result.target}`);
120
+ }
121
+ }
122
+ export function validateManagedFiles(root) {
123
+ const results = applyManagedFiles(root, 'check');
124
+ printResults(results);
125
+ const failures = results.filter((result) => result.action === 'drifted').length;
126
+ if (failures > 0) {
127
+ console.error('Managed monorepo files are out of date. Run: smoo monorepo update');
128
+ }
129
+ return failures;
130
+ }
@@ -0,0 +1,2 @@
1
+ export declare function validateNxSync(root: string): Promise<number>;
2
+ //# sourceMappingURL=nx-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nx-sync.d.ts","sourceRoot":"","sources":["../../src/monorepo/nx-sync.ts"],"names":[],"mappings":"AAEA,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE"}
@@ -0,0 +1,5 @@
1
+ import { runStatus } from '../lib/run.js';
2
+ export async function validateNxSync(root) {
3
+ const status = await runStatus('nx', ['sync:check'], root);
4
+ return status === 0 ? 0 : 1;
5
+ }
@@ -0,0 +1,3 @@
1
+ export declare function fixPackageHygiene(root: string): Promise<void>;
2
+ export declare function validatePackageHygiene(root: string): Promise<number>;
3
+ //# sourceMappingURL=package-hygiene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-hygiene.d.ts","sourceRoot":"","sources":["../../src/monorepo/package-hygiene.ts"],"names":[],"mappings":"AAEA,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnE;AAED,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAO1E"}
@@ -0,0 +1,12 @@
1
+ import { run, runStatus } from '../lib/run.js';
2
+ export async function fixPackageHygiene(root) {
3
+ await run('sherif', ['--fix', '--select', 'highest'], root);
4
+ }
5
+ export async function validatePackageHygiene(root) {
6
+ const status = await runStatus('sherif', [], root);
7
+ if (status !== 0) {
8
+ console.error('sherif package hygiene validation failed');
9
+ return 1;
10
+ }
11
+ return 0;
12
+ }