t44 0.2.0-rc.1
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.
Potentially problematic release.
This version of t44 might be problematic. Click here for more details.
- package/LICENSE.md +203 -0
- package/README.md +154 -0
- package/bin/activate +36 -0
- package/bin/activate.ts +30 -0
- package/bin/postinstall.sh +19 -0
- package/bin/shell +27 -0
- package/bin/t44 +27 -0
- package/caps/HomeRegistry.v0.ts +298 -0
- package/caps/OpenApiSchema.v0.ts +192 -0
- package/caps/ProjectDeployment.v0.ts +363 -0
- package/caps/ProjectDevelopment.v0.ts +246 -0
- package/caps/ProjectPublishing.v0.ts +307 -0
- package/caps/ProjectRack.v0.ts +128 -0
- package/caps/WorkspaceCli.v0.ts +391 -0
- package/caps/WorkspaceConfig.v0.ts +626 -0
- package/caps/WorkspaceConfig.yaml +53 -0
- package/caps/WorkspaceConnection.v0.ts +240 -0
- package/caps/WorkspaceEntityConfig.v0.ts +64 -0
- package/caps/WorkspaceEntityFact.v0.ts +193 -0
- package/caps/WorkspaceInfo.v0.ts +554 -0
- package/caps/WorkspaceInit.v0.ts +30 -0
- package/caps/WorkspaceKey.v0.ts +186 -0
- package/caps/WorkspaceProjects.v0.ts +455 -0
- package/caps/WorkspacePrompt.v0.ts +396 -0
- package/caps/WorkspaceShell.sh +39 -0
- package/caps/WorkspaceShell.v0.ts +104 -0
- package/caps/WorkspaceShell.yaml +65 -0
- package/caps/WorkspaceShellCli.v0.ts +109 -0
- package/caps/WorkspaceTest.v0.ts +167 -0
- package/caps/providers/LICENSE.md +8 -0
- package/caps/providers/README.md +2 -0
- package/caps/providers/bunny.net/ProjectDeployment.v0.ts +328 -0
- package/caps/providers/bunny.net/api-pull.v0.test.ts +319 -0
- package/caps/providers/bunny.net/api-pull.v0.ts +161 -0
- package/caps/providers/bunny.net/api-storage.v0.test.ts +168 -0
- package/caps/providers/bunny.net/api-storage.v0.ts +245 -0
- package/caps/providers/bunny.net/api.v0.ts +95 -0
- package/caps/providers/dynadot.com/ProjectDeployment.v0.ts +207 -0
- package/caps/providers/dynadot.com/api-domains.v0.test.ts +147 -0
- package/caps/providers/dynadot.com/api-domains.v0.ts +137 -0
- package/caps/providers/dynadot.com/api.v0.ts +88 -0
- package/caps/providers/git-scm.com/ProjectPublishing.v0.ts +231 -0
- package/caps/providers/github.com/ProjectPublishing.v0.ts +75 -0
- package/caps/providers/github.com/api.v0.ts +90 -0
- package/caps/providers/npmjs.com/ProjectPublishing.v0.ts +741 -0
- package/caps/providers/vercel.com/ProjectDeployment.v0.ts +339 -0
- package/caps/providers/vercel.com/api.v0.test.ts +67 -0
- package/caps/providers/vercel.com/api.v0.ts +132 -0
- package/caps/providers/vercel.com/bun.lock +194 -0
- package/caps/providers/vercel.com/package.json +10 -0
- package/caps/providers/vercel.com/project.v0.test.ts +108 -0
- package/caps/providers/vercel.com/project.v0.ts +150 -0
- package/caps/providers/vercel.com/tsconfig.json +28 -0
- package/docs/Overview.drawio +189 -0
- package/docs/Overview.svg +4 -0
- package/lib/crypto.ts +53 -0
- package/lib/openapi.ts +132 -0
- package/lib/ucan.ts +137 -0
- package/package.json +41 -0
- package/structs/HomeRegistryConfig.v0.ts +27 -0
- package/structs/ProjectDeploymentConfig.v0.ts +27 -0
- package/structs/ProjectDeploymentFact.v0.ts +110 -0
- package/structs/ProjectPublishingFact.v0.ts +69 -0
- package/structs/ProjectRackConfig.v0.ts +27 -0
- package/structs/WorkspaceCliConfig.v0.ts +27 -0
- package/structs/WorkspaceConfig.v0.ts +27 -0
- package/structs/WorkspaceKeyConfig.v0.ts +27 -0
- package/structs/WorkspaceMappings.v0.ts +27 -0
- package/structs/WorkspaceProjectsConfig.v0.ts +27 -0
- package/structs/WorkspaceRepositories.v0.ts +27 -0
- package/structs/WorkspaceShellConfig.v0.ts +45 -0
- package/structs/providers/LICENSE.md +8 -0
- package/structs/providers/README.md +2 -0
- package/structs/providers/bunny.net/ProjectDeploymentFact.v0.ts +41 -0
- package/structs/providers/bunny.net/WorkspaceConnectionConfig.v0.ts +42 -0
- package/structs/providers/dynadot.com/DomainFact.v0.ts +146 -0
- package/structs/providers/dynadot.com/WorkspaceConnectionConfig.v0.ts +41 -0
- package/structs/providers/git-scm.com/ProjectPublishingFact.v0.ts +46 -0
- package/structs/providers/github.com/ProjectPublishingFact.v0.ts +52 -0
- package/structs/providers/github.com/WorkspaceConnectionConfig.v0.ts +42 -0
- package/structs/providers/npmjs.com/ProjectPublishingFact.v0.ts +48 -0
- package/structs/providers/vercel.com/ProjectDeploymentFact.v0.ts +38 -0
- package/structs/providers/vercel.com/WorkspaceConnectionConfig.v0.ts +48 -0
- package/tsconfig.json +28 -0
- package/workspace-rt.ts +134 -0
- package/workspace.yaml +5 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
export async function capsule({
|
|
2
|
+
encapsulate,
|
|
3
|
+
CapsulePropertyTypes,
|
|
4
|
+
makeImportStack
|
|
5
|
+
}: {
|
|
6
|
+
encapsulate: any
|
|
7
|
+
CapsulePropertyTypes: any
|
|
8
|
+
makeImportStack: any
|
|
9
|
+
}) {
|
|
10
|
+
return encapsulate({
|
|
11
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
12
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
13
|
+
'#t44/structs/WorkspaceConfig.v0': {
|
|
14
|
+
as: '$WorkspaceConfig'
|
|
15
|
+
},
|
|
16
|
+
'#t44/structs/WorkspaceKeyConfig.v0': {
|
|
17
|
+
as: '$WorkspaceKeyConfig'
|
|
18
|
+
},
|
|
19
|
+
'#': {
|
|
20
|
+
WorkspacePrompt: {
|
|
21
|
+
type: CapsulePropertyTypes.Mapping,
|
|
22
|
+
value: 't44/caps/WorkspacePrompt.v0'
|
|
23
|
+
},
|
|
24
|
+
HomeRegistry: {
|
|
25
|
+
type: CapsulePropertyTypes.Mapping,
|
|
26
|
+
value: 't44/caps/HomeRegistry.v0'
|
|
27
|
+
},
|
|
28
|
+
ensureKey: {
|
|
29
|
+
type: CapsulePropertyTypes.Function,
|
|
30
|
+
value: async function (this: any): Promise<{ keyName: string; keyPath: string }> {
|
|
31
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
32
|
+
const keyConfig = await this.$WorkspaceKeyConfig.config
|
|
33
|
+
|
|
34
|
+
// Check if key is already set in config (object format: { name, identifier })
|
|
35
|
+
if (keyConfig?.name && keyConfig?.identifier) {
|
|
36
|
+
const keyExists = await this.HomeRegistry.keyExists(keyConfig.name)
|
|
37
|
+
|
|
38
|
+
if (keyExists) {
|
|
39
|
+
const keyPath = await this.HomeRegistry.getKeyPath(keyConfig.name)
|
|
40
|
+
return { keyName: keyConfig.name, keyPath }
|
|
41
|
+
} else {
|
|
42
|
+
const chalk = (await import('chalk')).default
|
|
43
|
+
const keyPath = await this.HomeRegistry.getKeyPath(keyConfig.name)
|
|
44
|
+
console.log(chalk.yellow(`\n⚠️ Workspace key '${keyConfig.name}' is configured but key file not found at:`))
|
|
45
|
+
console.log(chalk.yellow(` ${keyPath}\n`))
|
|
46
|
+
// Fall through to generate the key
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let keyName: string
|
|
51
|
+
|
|
52
|
+
const keyConfigStructKey = '#t44/structs/WorkspaceKeyConfig.v0'
|
|
53
|
+
if (!keyConfig?.name) {
|
|
54
|
+
keyName = await this.WorkspacePrompt.setupPrompt({
|
|
55
|
+
title: '🔐 Workspace Key Setup',
|
|
56
|
+
description: [
|
|
57
|
+
`Workspace: ${workspaceConfig?.name || 'unknown'}`,
|
|
58
|
+
`Root: ${workspaceConfig?.rootDir || 'unknown'}`,
|
|
59
|
+
'',
|
|
60
|
+
'All credentials in this workspace are encrypted with a workspace key.',
|
|
61
|
+
'This key can be shared across multiple workspaces.',
|
|
62
|
+
'',
|
|
63
|
+
],
|
|
64
|
+
message: 'Enter a name for the workspace key:',
|
|
65
|
+
defaultValue: 'genesis',
|
|
66
|
+
validate: (input: string) => {
|
|
67
|
+
if (!input || input.trim().length === 0) {
|
|
68
|
+
return 'Key name cannot be empty'
|
|
69
|
+
}
|
|
70
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
|
|
71
|
+
return 'Key name can only contain letters, numbers, underscores, and hyphens'
|
|
72
|
+
}
|
|
73
|
+
return true
|
|
74
|
+
},
|
|
75
|
+
configPath: [keyConfigStructKey],
|
|
76
|
+
onSuccess: async () => {
|
|
77
|
+
// Don't write to config yet — we write the full object after key generation
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
} else {
|
|
81
|
+
keyName = keyConfig.name
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check if key already exists in registry
|
|
85
|
+
let keyData = await this.HomeRegistry.getKey(keyName)
|
|
86
|
+
|
|
87
|
+
if (!keyData) {
|
|
88
|
+
const chalk = (await import('chalk')).default
|
|
89
|
+
// Generate Ed25519 key pair using UCAN library
|
|
90
|
+
console.log(chalk.cyan(`\n Generating Ed25519 key '${keyName}'...\n`))
|
|
91
|
+
|
|
92
|
+
const { generateKeypair } = await import('../lib/ucan.js')
|
|
93
|
+
const { did, privateKey } = await generateKeypair()
|
|
94
|
+
|
|
95
|
+
keyData = {
|
|
96
|
+
did,
|
|
97
|
+
privateKey,
|
|
98
|
+
createdAt: new Date().toISOString()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const keyPath = await this.HomeRegistry.setKey(keyName, keyData)
|
|
102
|
+
|
|
103
|
+
console.log(chalk.green(` ✓ Key generated and saved to:`))
|
|
104
|
+
console.log(chalk.green(` ${keyPath}`))
|
|
105
|
+
console.log(chalk.green(` ✓ DID: ${keyData.did}\n`))
|
|
106
|
+
} else {
|
|
107
|
+
const chalk = (await import('chalk')).default
|
|
108
|
+
const keyPath = await this.HomeRegistry.getKeyPath(keyName)
|
|
109
|
+
console.log(chalk.green(`\n ✓ Using existing key at:`))
|
|
110
|
+
console.log(chalk.green(` ${keyPath}`))
|
|
111
|
+
console.log(chalk.green(` ✓ DID: ${keyData.did}\n`))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Store key as object { name, identifier } in key config struct
|
|
115
|
+
await this.$WorkspaceKeyConfig.setConfigValue(['name'], keyName)
|
|
116
|
+
await this.$WorkspaceKeyConfig.setConfigValue(['identifier'], keyData.did)
|
|
117
|
+
|
|
118
|
+
const keyPath = await this.HomeRegistry.getKeyPath(keyName)
|
|
119
|
+
return { keyName, keyPath }
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
getKeyPath: {
|
|
123
|
+
type: CapsulePropertyTypes.Function,
|
|
124
|
+
value: async function (this: any): Promise<string | null> {
|
|
125
|
+
const keyConfig = await this.$WorkspaceKeyConfig.config
|
|
126
|
+
|
|
127
|
+
if (!keyConfig?.name) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this.HomeRegistry.getKeyPath(keyConfig.name)
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
getKey: {
|
|
135
|
+
type: CapsulePropertyTypes.Function,
|
|
136
|
+
value: async function (this: any): Promise<{ did: string; privateKey: string }> {
|
|
137
|
+
const keyConfig = await this.$WorkspaceKeyConfig.config
|
|
138
|
+
|
|
139
|
+
if (!keyConfig?.name) {
|
|
140
|
+
throw new Error('No workspace key configured. Run ensureKey() first.')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const keyData = await this.HomeRegistry.getKey(keyConfig.name)
|
|
144
|
+
|
|
145
|
+
if (!keyData) {
|
|
146
|
+
throw new Error(`Workspace key '${keyConfig.name}' not found in registry. Run ensureKey() first.`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
did: keyData.did,
|
|
151
|
+
privateKey: keyData.privateKey
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
getDid: {
|
|
156
|
+
type: CapsulePropertyTypes.Function,
|
|
157
|
+
value: async function (this: any): Promise<string> {
|
|
158
|
+
const { did } = await this.getKey()
|
|
159
|
+
return did
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
encryptString: {
|
|
163
|
+
type: CapsulePropertyTypes.Function,
|
|
164
|
+
value: async function (this: any, plaintext: string): Promise<string> {
|
|
165
|
+
const { privateKey } = await this.getKey()
|
|
166
|
+
const { encryptString } = await import('../lib/crypto.js')
|
|
167
|
+
return encryptString(plaintext, privateKey)
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
decryptString: {
|
|
171
|
+
type: CapsulePropertyTypes.Function,
|
|
172
|
+
value: async function (this: any, ciphertext: string): Promise<string> {
|
|
173
|
+
const { privateKey } = await this.getKey()
|
|
174
|
+
const { decryptString } = await import('../lib/crypto.js')
|
|
175
|
+
return decryptString(ciphertext, privateKey)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}, {
|
|
181
|
+
importMeta: import.meta,
|
|
182
|
+
importStack: makeImportStack(),
|
|
183
|
+
capsuleName: capsule['#'],
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
capsule['#'] = 't44/caps/WorkspaceKey.v0'
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
|
|
2
|
+
import { join, resolve, relative } from 'path'
|
|
3
|
+
import { readdir, stat } from 'fs/promises'
|
|
4
|
+
import { $ } from 'bun'
|
|
5
|
+
|
|
6
|
+
export async function capsule({
|
|
7
|
+
encapsulate,
|
|
8
|
+
CapsulePropertyTypes,
|
|
9
|
+
makeImportStack
|
|
10
|
+
}: {
|
|
11
|
+
encapsulate: any
|
|
12
|
+
CapsulePropertyTypes: any
|
|
13
|
+
makeImportStack: any
|
|
14
|
+
}) {
|
|
15
|
+
return encapsulate({
|
|
16
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
17
|
+
'#@stream44.studio/encapsulate/structs/Capsule.v0': {},
|
|
18
|
+
'#t44/structs/WorkspaceConfig.v0': {
|
|
19
|
+
as: '$WorkspaceConfig'
|
|
20
|
+
},
|
|
21
|
+
'#t44/structs/WorkspaceProjectsConfig.v0': {
|
|
22
|
+
as: '$WorkspaceProjectsConfig',
|
|
23
|
+
},
|
|
24
|
+
'#t44/structs/ProjectDeploymentConfig.v0': {
|
|
25
|
+
as: '$ProjectDeploymentConfig',
|
|
26
|
+
},
|
|
27
|
+
'#t44/structs/WorkspaceRepositories.v0': {
|
|
28
|
+
as: '$WorkspaceRepositories'
|
|
29
|
+
},
|
|
30
|
+
'#': {
|
|
31
|
+
list: {
|
|
32
|
+
type: CapsulePropertyTypes.GetterFunction,
|
|
33
|
+
value: async function (this: any): Promise<Record<string, { sourceDir: string, deployments: Record<string, any>, repositories: Record<string, any> }>> {
|
|
34
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
35
|
+
const workspaceRootDir = workspaceConfig?.rootDir
|
|
36
|
+
|
|
37
|
+
if (!workspaceRootDir) {
|
|
38
|
+
throw new Error('Workspace root directory not configured')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const configFilepath = join(workspaceRootDir, '.workspace/workspace.yaml')
|
|
42
|
+
|
|
43
|
+
// Read existing projects config
|
|
44
|
+
const projectsConfig = await this.$WorkspaceProjectsConfig.config
|
|
45
|
+
const configuredProjects: Record<string, { sourceDir: string }> = projectsConfig?.projects || {}
|
|
46
|
+
|
|
47
|
+
// Scan workspace root for project directories
|
|
48
|
+
const entries = await readdir(workspaceRootDir, { withFileTypes: true })
|
|
49
|
+
const scannedDirs: string[] = []
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.isDirectory()) continue
|
|
53
|
+
if (entry.name.startsWith('.')) continue
|
|
54
|
+
if (entry.name === 'node_modules') continue
|
|
55
|
+
if (entry.name === '___') continue
|
|
56
|
+
scannedDirs.push(entry.name)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Pre-fill config with scanned projects that are not yet configured
|
|
60
|
+
for (const dirName of scannedDirs) {
|
|
61
|
+
if (!configuredProjects[dirName]) {
|
|
62
|
+
const sourceDir = `resolve('\${__dirname}/../${dirName}')`
|
|
63
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', dirName, 'sourceDir'], sourceDir)
|
|
64
|
+
configuredProjects[dirName] = { sourceDir: join(workspaceRootDir, dirName) }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build projects from config values, validating each
|
|
69
|
+
const projects: Record<string, { sourceDir: string, git: any, deployments: Record<string, any>, repositories: Record<string, any> }> = {}
|
|
70
|
+
|
|
71
|
+
const sortedProjectEntries = Object.entries(configuredProjects).sort(([a], [b]) => a.localeCompare(b))
|
|
72
|
+
for (const [projectName, projectConfig] of sortedProjectEntries) {
|
|
73
|
+
const typedConfig = projectConfig as any
|
|
74
|
+
|
|
75
|
+
if (!typedConfig.sourceDir) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Project '${projectName}' has no sourceDir configured.\n` +
|
|
78
|
+
` Fix in: ${configFilepath}\n` +
|
|
79
|
+
` Under: '#t44/structs/WorkspaceProjectsConfig.v0' → projects → ${projectName}`
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const resolvedSourceDir = resolve(typedConfig.sourceDir)
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const dirStat = await stat(resolvedSourceDir)
|
|
87
|
+
if (!dirStat.isDirectory()) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Project '${projectName}' sourceDir is not a directory: ${resolvedSourceDir}\n` +
|
|
90
|
+
` Fix in: ${configFilepath}\n` +
|
|
91
|
+
` Under: '#t44/structs/WorkspaceProjectsConfig.v0' → projects → ${projectName} → sourceDir`
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
} catch (err: any) {
|
|
95
|
+
if (err.code === 'ENOENT') {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Project '${projectName}' sourceDir does not exist: ${resolvedSourceDir}\n` +
|
|
98
|
+
` Fix in: ${configFilepath}\n` +
|
|
99
|
+
` Under: '#t44/structs/WorkspaceProjectsConfig.v0' → projects → ${projectName} → sourceDir`
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
throw err
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
projects[projectName] = {
|
|
106
|
+
sourceDir: resolvedSourceDir,
|
|
107
|
+
git: typedConfig.git !== undefined ? typedConfig.git : undefined,
|
|
108
|
+
deployments: {},
|
|
109
|
+
repositories: {}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Map deployments to projects
|
|
114
|
+
const deploymentConfig = await this.$ProjectDeploymentConfig.config
|
|
115
|
+
if (deploymentConfig?.deployments) {
|
|
116
|
+
for (const [deploymentName, deploymentAliases] of Object.entries(deploymentConfig.deployments)) {
|
|
117
|
+
const aliases = deploymentAliases as Record<string, any>
|
|
118
|
+
// Find the project by checking sourceDir of any alias
|
|
119
|
+
let mappedProject: string | null = null
|
|
120
|
+
for (const [aliasName, aliasConfig] of Object.entries(aliases)) {
|
|
121
|
+
if (aliasConfig.sourceDir) {
|
|
122
|
+
const resolvedSourceDir = resolve(aliasConfig.sourceDir)
|
|
123
|
+
const relPath = relative(workspaceRootDir, resolvedSourceDir)
|
|
124
|
+
const topDir = relPath.split('/')[0]
|
|
125
|
+
if (projects[topDir]) {
|
|
126
|
+
mappedProject = topDir
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!mappedProject) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Deployment '${deploymentName}' does not map to any workspace project.\n` +
|
|
135
|
+
` Ensure at least one alias has a valid sourceDir pointing to a project directory.\n` +
|
|
136
|
+
` Known projects: ${Object.keys(projects).join(', ')}\n` +
|
|
137
|
+
` Fix in: ${configFilepath}`
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
projects[mappedProject].deployments[deploymentName] = aliases
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Map repositories to projects
|
|
146
|
+
const repositoriesConfig = await this.$WorkspaceRepositories.config
|
|
147
|
+
if (repositoriesConfig?.repositories) {
|
|
148
|
+
for (const [repoName, repoConfig] of Object.entries(repositoriesConfig.repositories)) {
|
|
149
|
+
const typedConfig = repoConfig as any
|
|
150
|
+
if (typedConfig.sourceDir) {
|
|
151
|
+
const resolvedSourceDir = resolve(typedConfig.sourceDir)
|
|
152
|
+
const relPath = relative(workspaceRootDir, resolvedSourceDir)
|
|
153
|
+
const topDir = relPath.split('/')[0]
|
|
154
|
+
if (!projects[topDir]) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`Repository '${repoName}' sourceDir '${typedConfig.sourceDir}' does not map to any workspace project '${topDir}'.\n` +
|
|
157
|
+
` Known projects: ${Object.keys(projects).join(', ')}\n` +
|
|
158
|
+
` Fix in: ${configFilepath}`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
projects[topDir].repositories[repoName] = typedConfig
|
|
162
|
+
} else {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Repository '${repoName}' has no sourceDir configured.\n` +
|
|
165
|
+
` Fix in: ${configFilepath}`
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return projects
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
gatherGitInfo: {
|
|
175
|
+
type: CapsulePropertyTypes.Function,
|
|
176
|
+
value: async function (this: any, { now }: { now?: boolean } = {}): Promise<void> {
|
|
177
|
+
const projects = await this.list
|
|
178
|
+
|
|
179
|
+
for (const [projectName, projectInfo] of Object.entries(projects)) {
|
|
180
|
+
const project = projectInfo as any
|
|
181
|
+
const sourceDir = project.sourceDir
|
|
182
|
+
|
|
183
|
+
// If git info is already in config and --now is not passed, skip
|
|
184
|
+
if (project.git !== undefined && !now) {
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if this is a git repo
|
|
189
|
+
try {
|
|
190
|
+
const gitDirCheck = await $`git -C ${sourceDir} rev-parse --git-dir`.quiet().nothrow()
|
|
191
|
+
if (gitDirCheck.exitCode !== 0) {
|
|
192
|
+
// Not a git repo
|
|
193
|
+
if (project.git === undefined) {
|
|
194
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], false)
|
|
195
|
+
}
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Get first commit hash
|
|
200
|
+
const firstCommitResult = await $`git -C ${sourceDir} rev-list --max-parents=0 HEAD`.quiet().nothrow()
|
|
201
|
+
if (firstCommitResult.exitCode !== 0) {
|
|
202
|
+
// No commits yet
|
|
203
|
+
if (project.git === undefined) {
|
|
204
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], false)
|
|
205
|
+
}
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const firstCommitHash = firstCommitResult.text().trim().split('\n')[0]
|
|
210
|
+
|
|
211
|
+
// Get first commit date (createdAt)
|
|
212
|
+
const createdAtResult = await $`git -C ${sourceDir} show -s --format=%aI ${firstCommitHash}`.quiet().nothrow()
|
|
213
|
+
const createdAt = createdAtResult.exitCode === 0 ? createdAtResult.text().trim() : null
|
|
214
|
+
|
|
215
|
+
// Get first commit author details
|
|
216
|
+
const authorNameResult = await $`git -C ${sourceDir} show -s --format=%an ${firstCommitHash}`.quiet().nothrow()
|
|
217
|
+
const authorEmailResult = await $`git -C ${sourceDir} show -s --format=%ae ${firstCommitHash}`.quiet().nothrow()
|
|
218
|
+
const firstCommitAuthor: Record<string, string> = {}
|
|
219
|
+
if (authorNameResult.exitCode === 0) {
|
|
220
|
+
firstCommitAuthor.name = authorNameResult.text().trim()
|
|
221
|
+
}
|
|
222
|
+
if (authorEmailResult.exitCode === 0) {
|
|
223
|
+
firstCommitAuthor.email = authorEmailResult.text().trim()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get remotes
|
|
227
|
+
const remotesResult = await $`git -C ${sourceDir} remote -v`.quiet().nothrow()
|
|
228
|
+
const remotes: Record<string, string> = {}
|
|
229
|
+
if (remotesResult.exitCode === 0) {
|
|
230
|
+
const lines = remotesResult.text().trim().split('\n').filter(Boolean)
|
|
231
|
+
for (const line of lines) {
|
|
232
|
+
const match = line.match(/^(\S+)\s+(\S+)\s+\(fetch\)$/)
|
|
233
|
+
if (match) {
|
|
234
|
+
remotes[match[1]] = match[2]
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// If --now is passed, sync remotes between config and git repo
|
|
240
|
+
if (now && project.git && typeof project.git === 'object' && project.git.remotes) {
|
|
241
|
+
for (const [remoteName, remoteUri] of Object.entries(project.git.remotes)) {
|
|
242
|
+
if (!remotes[remoteName]) {
|
|
243
|
+
// Remote exists in config but not in git repo — add it
|
|
244
|
+
const addResult = await $`git -C ${sourceDir} remote add ${remoteName} ${remoteUri as string}`.quiet().nothrow()
|
|
245
|
+
if (addResult.exitCode === 0) {
|
|
246
|
+
remotes[remoteName] = remoteUri as string
|
|
247
|
+
}
|
|
248
|
+
} else if (remotes[remoteName] !== remoteUri) {
|
|
249
|
+
// Remote URL in config differs from git — update git to match config
|
|
250
|
+
const setUrlResult = await $`git -C ${sourceDir} remote set-url ${remoteName} ${remoteUri as string}`.quiet().nothrow()
|
|
251
|
+
if (setUrlResult.exitCode === 0) {
|
|
252
|
+
remotes[remoteName] = remoteUri as string
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const gitInfo: Record<string, any> = {
|
|
259
|
+
firstCommitHash,
|
|
260
|
+
createdAt,
|
|
261
|
+
firstCommitAuthor,
|
|
262
|
+
remotes
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], gitInfo)
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
// If git commands fail entirely, mark as false
|
|
268
|
+
if (project.git === undefined) {
|
|
269
|
+
await this.$WorkspaceProjectsConfig.setConfigValue(['projects', projectName, 'git'], false)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
resolveMatchingRepositories: {
|
|
276
|
+
type: CapsulePropertyTypes.Function,
|
|
277
|
+
value: async function (this: any, { workspaceProject, repositories }: {
|
|
278
|
+
workspaceProject: string
|
|
279
|
+
repositories: Record<string, any>
|
|
280
|
+
}): Promise<Record<string, any>> {
|
|
281
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
282
|
+
const workspaceRootDir = workspaceConfig?.rootDir
|
|
283
|
+
const currentDir = process.cwd()
|
|
284
|
+
|
|
285
|
+
let matchingRepositories: Record<string, any> = {}
|
|
286
|
+
|
|
287
|
+
// Strategy 1: Try prefix matching on repository names
|
|
288
|
+
const prefixMatches: string[] = []
|
|
289
|
+
for (const repoName of Object.keys(repositories)) {
|
|
290
|
+
if (repoName.startsWith(workspaceProject)) {
|
|
291
|
+
prefixMatches.push(repoName)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (prefixMatches.length > 1) {
|
|
296
|
+
const chalk = (await import('chalk')).default
|
|
297
|
+
console.log(chalk.red(`\nError: Multiple repositories match prefix '${workspaceProject}':\n`))
|
|
298
|
+
for (const match of prefixMatches) {
|
|
299
|
+
console.log(chalk.gray(` - ${match}`))
|
|
300
|
+
}
|
|
301
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
302
|
+
throw new Error(`Multiple repositories match prefix: ${workspaceProject}`)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (prefixMatches.length === 1) {
|
|
306
|
+
matchingRepositories[prefixMatches[0]] = repositories[prefixMatches[0]]
|
|
307
|
+
return matchingRepositories
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Strategy 2: Try path matching (absolute or relative from current directory)
|
|
311
|
+
let targetPath: string
|
|
312
|
+
if (workspaceProject.startsWith('/')) {
|
|
313
|
+
targetPath = workspaceProject
|
|
314
|
+
} else {
|
|
315
|
+
targetPath = resolve(currentDir, workspaceProject)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const [repoName, repoConfig] of Object.entries(repositories)) {
|
|
319
|
+
if ((repoConfig as any).sourceDir) {
|
|
320
|
+
const sourceDirPath = resolve((repoConfig as any).sourceDir)
|
|
321
|
+
const rel = relative(targetPath, sourceDirPath)
|
|
322
|
+
|
|
323
|
+
const isWithinOrEqual = rel === '' || !rel.startsWith('..')
|
|
324
|
+
|
|
325
|
+
if (isWithinOrEqual) {
|
|
326
|
+
matchingRepositories[repoName] = repoConfig
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (Object.keys(matchingRepositories).length === 0) {
|
|
332
|
+
const chalk = (await import('chalk')).default
|
|
333
|
+
console.log(chalk.red(`\nError: No repositories found matching '${workspaceProject}'.\n`))
|
|
334
|
+
console.log(chalk.gray('Available repositories:'))
|
|
335
|
+
for (const repoName of Object.keys(repositories)) {
|
|
336
|
+
console.log(chalk.gray(` - ${repoName}`))
|
|
337
|
+
}
|
|
338
|
+
console.log('')
|
|
339
|
+
throw new Error(`No repositories found matching: ${workspaceProject}`)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return matchingRepositories
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
resolveMatchingDeployments: {
|
|
346
|
+
type: CapsulePropertyTypes.Function,
|
|
347
|
+
value: async function (this: any, { workspaceProject, deployments }: {
|
|
348
|
+
workspaceProject: string
|
|
349
|
+
deployments: Record<string, any>
|
|
350
|
+
}): Promise<Record<string, any>> {
|
|
351
|
+
const workspaceConfig = await this.$WorkspaceConfig.config
|
|
352
|
+
const workspaceRootDir = workspaceConfig?.rootDir
|
|
353
|
+
const currentDir = process.cwd()
|
|
354
|
+
|
|
355
|
+
let matchingDeployments: Record<string, any> = {}
|
|
356
|
+
|
|
357
|
+
// Strategy 1: Try prefix matching on project names
|
|
358
|
+
const prefixMatches: string[] = []
|
|
359
|
+
for (const projectName of Object.keys(deployments)) {
|
|
360
|
+
if (projectName.startsWith(workspaceProject)) {
|
|
361
|
+
prefixMatches.push(projectName)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (prefixMatches.length > 1) {
|
|
366
|
+
const chalk = (await import('chalk')).default
|
|
367
|
+
console.log(chalk.red(`\nError: Multiple projects match prefix '${workspaceProject}':\n`))
|
|
368
|
+
for (const match of prefixMatches) {
|
|
369
|
+
console.log(chalk.gray(` - ${match}`))
|
|
370
|
+
}
|
|
371
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
372
|
+
throw new Error(`Multiple projects match prefix: ${workspaceProject}`)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (prefixMatches.length === 1) {
|
|
376
|
+
matchingDeployments[prefixMatches[0]] = deployments[prefixMatches[0]]
|
|
377
|
+
return matchingDeployments
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Strategy 2: Try path matching (absolute or relative from current directory)
|
|
381
|
+
let targetPath: string
|
|
382
|
+
if (workspaceProject.startsWith('/')) {
|
|
383
|
+
targetPath = workspaceProject
|
|
384
|
+
} else {
|
|
385
|
+
targetPath = resolve(currentDir, workspaceProject)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for (const [projectName, projectAliases] of Object.entries(deployments)) {
|
|
389
|
+
for (const [alias, aliasConfig] of Object.entries(projectAliases as Record<string, any>)) {
|
|
390
|
+
if (aliasConfig.sourceDir) {
|
|
391
|
+
const sourceDirPath = resolve(aliasConfig.sourceDir)
|
|
392
|
+
const rel = relative(targetPath, sourceDirPath)
|
|
393
|
+
|
|
394
|
+
const isWithinOrEqual = rel === '' || !rel.startsWith('..')
|
|
395
|
+
|
|
396
|
+
if (isWithinOrEqual) {
|
|
397
|
+
if (!matchingDeployments[projectName]) {
|
|
398
|
+
matchingDeployments[projectName] = {}
|
|
399
|
+
}
|
|
400
|
+
matchingDeployments[projectName][alias] = aliasConfig
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (Object.keys(matchingDeployments).length > 1) {
|
|
407
|
+
const chalk = (await import('chalk')).default
|
|
408
|
+
const pathMatches = Object.keys(matchingDeployments)
|
|
409
|
+
console.log(chalk.red(`\nError: Multiple projects match path '${workspaceProject}':\n`))
|
|
410
|
+
for (const match of pathMatches) {
|
|
411
|
+
console.log(chalk.gray(` - ${match}`))
|
|
412
|
+
}
|
|
413
|
+
console.log(chalk.red('\nPlease be more specific.\n'))
|
|
414
|
+
throw new Error(`Multiple projects match path: ${workspaceProject}`)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (Object.keys(matchingDeployments).length === 0) {
|
|
418
|
+
const chalk = (await import('chalk')).default
|
|
419
|
+
console.log(chalk.red(`\nError: No deployments found matching '${workspaceProject}'.\n`))
|
|
420
|
+
console.log(chalk.gray('Available projects:'))
|
|
421
|
+
for (const projectName of Object.keys(deployments)) {
|
|
422
|
+
console.log(chalk.gray(` - ${projectName}`))
|
|
423
|
+
}
|
|
424
|
+
console.log('')
|
|
425
|
+
throw new Error(`No deployments found matching: ${workspaceProject}`)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return matchingDeployments
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
findProjectForPath: {
|
|
432
|
+
type: CapsulePropertyTypes.Function,
|
|
433
|
+
value: async function (this: any, { targetPath }: { targetPath: string }): Promise<string | null> {
|
|
434
|
+
const projects = await this.list
|
|
435
|
+
const resolvedTarget = resolve(targetPath)
|
|
436
|
+
|
|
437
|
+
for (const [projectName, projectInfo] of Object.entries(projects)) {
|
|
438
|
+
const projectDir = (projectInfo as any).sourceDir
|
|
439
|
+
if (resolvedTarget === projectDir || resolvedTarget.startsWith(projectDir + '/')) {
|
|
440
|
+
return projectName
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return null
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}, {
|
|
450
|
+
importMeta: import.meta,
|
|
451
|
+
importStack: makeImportStack(),
|
|
452
|
+
capsuleName: capsule['#'],
|
|
453
|
+
})
|
|
454
|
+
}
|
|
455
|
+
capsule['#'] = 't44/caps/WorkspaceProjects.v0'
|