gitspace 0.2.0-rc.5 → 0.2.0-rc.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bun.lock +8 -0
- package/docs/SITE_DOCS_FIGMA_MAKE.md +8 -8
- package/package.json +5 -5
- package/src/commands/remove.ts +66 -50
- package/src/core/__tests__/workspace.test.ts +149 -0
- package/src/core/bundle.ts +1 -1
- package/src/core/workspace.ts +249 -0
- package/src/index.ts +7 -1
- package/src/lib/remote-session/session-handler.ts +31 -21
- package/src/tui/app.tsx +178 -25
- package/src/utils/__tests__/run-scripts.test.ts +306 -0
- package/src/utils/__tests__/run-workspace-scripts.test.ts +252 -0
- package/src/utils/run-scripts.ts +27 -3
- package/src/utils/run-workspace-scripts.ts +89 -0
- package/src/utils/sanitize.ts +64 -0
- package/src/utils/workspace-state.ts +17 -1
- package/src/version.generated.d.ts +2 -0
- package/.claude/settings.local.json +0 -25
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace script execution with phase tracking
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the logic for running pre/setup/select scripts with
|
|
5
|
+
* proper error handling and phase identification.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { runScriptsInTerminal, type RunScriptsOptions } from './run-scripts';
|
|
9
|
+
import { hasSetupBeenRun, markSetupComplete } from './workspace-state';
|
|
10
|
+
import { getScriptsPhaseDir, readProjectConfig } from '../core/config';
|
|
11
|
+
import { getProjectSecrets } from './secrets';
|
|
12
|
+
|
|
13
|
+
export type ScriptPhase = 'pre' | 'setup' | 'select';
|
|
14
|
+
|
|
15
|
+
export interface RunWorkspaceScriptsOptions {
|
|
16
|
+
projectName: string;
|
|
17
|
+
workspacePath: string;
|
|
18
|
+
workspaceName: string;
|
|
19
|
+
repository: string;
|
|
20
|
+
/** If true, scripts can prompt for input. If false (default), stdin is closed. */
|
|
21
|
+
interactive?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type RunWorkspaceScriptsResult =
|
|
25
|
+
| { success: true }
|
|
26
|
+
| { success: false; phase: ScriptPhase; error: string };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run workspace scripts based on setup state.
|
|
30
|
+
*
|
|
31
|
+
* - If setup has already been run: runs select scripts only
|
|
32
|
+
* - If first time: runs pre scripts, then setup scripts, then marks setup complete
|
|
33
|
+
*
|
|
34
|
+
* Returns success or failure with the specific phase that failed.
|
|
35
|
+
*/
|
|
36
|
+
export async function runWorkspaceScripts(
|
|
37
|
+
options: RunWorkspaceScriptsOptions
|
|
38
|
+
): Promise<RunWorkspaceScriptsResult> {
|
|
39
|
+
const { projectName, workspacePath, workspaceName, repository, interactive = false } = options;
|
|
40
|
+
|
|
41
|
+
// Read project config for bundle values and secrets
|
|
42
|
+
const config = readProjectConfig(projectName);
|
|
43
|
+
|
|
44
|
+
// Build script options
|
|
45
|
+
const scriptOptions: RunScriptsOptions = {
|
|
46
|
+
bundleValues: config.bundleValues,
|
|
47
|
+
nonInteractive: !interactive,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Fetch secrets from OS keychain if we have secret keys
|
|
51
|
+
if (config.bundleSecretKeys && config.bundleSecretKeys.length > 0) {
|
|
52
|
+
scriptOptions.bundleSecrets = await getProjectSecrets(projectName, config.bundleSecretKeys);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if setup has been run for this workspace
|
|
56
|
+
const setupAlreadyRun = hasSetupBeenRun(workspacePath);
|
|
57
|
+
|
|
58
|
+
if (setupAlreadyRun) {
|
|
59
|
+
// Run select scripts for existing workspace
|
|
60
|
+
const selectScriptsDir = getScriptsPhaseDir(projectName, 'select');
|
|
61
|
+
try {
|
|
62
|
+
await runScriptsInTerminal(selectScriptsDir, workspacePath, workspaceName, repository, scriptOptions);
|
|
63
|
+
return { success: true };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return { success: false, phase: 'select', error: message };
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// First time accessing this workspace - run pre scripts, then setup scripts
|
|
70
|
+
let preScriptsSucceeded = false;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const preScriptsDir = getScriptsPhaseDir(projectName, 'pre');
|
|
74
|
+
await runScriptsInTerminal(preScriptsDir, workspacePath, workspaceName, repository, scriptOptions);
|
|
75
|
+
preScriptsSucceeded = true;
|
|
76
|
+
|
|
77
|
+
const setupScriptsDir = getScriptsPhaseDir(projectName, 'setup');
|
|
78
|
+
await runScriptsInTerminal(setupScriptsDir, workspacePath, workspaceName, repository, scriptOptions);
|
|
79
|
+
|
|
80
|
+
// Only mark complete if both phases succeeded
|
|
81
|
+
markSetupComplete(workspacePath);
|
|
82
|
+
return { success: true };
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const phase: ScriptPhase = preScriptsSucceeded ? 'setup' : 'pre';
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
return { success: false, phase, error: message };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/utils/sanitize.ts
CHANGED
|
@@ -83,6 +83,70 @@ export function isValidWorkspaceName(name: string): boolean {
|
|
|
83
83
|
return true;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Validate a git branch name according to git-check-ref-format rules
|
|
88
|
+
* See: https://git-scm.com/docs/git-check-ref-format
|
|
89
|
+
*
|
|
90
|
+
* @param name Branch name to validate
|
|
91
|
+
* @returns true if valid, false otherwise
|
|
92
|
+
*/
|
|
93
|
+
export function isValidBranchName(name: string): boolean {
|
|
94
|
+
if (!name || name.length === 0) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Cannot have two consecutive dots
|
|
99
|
+
if (name.includes('..')) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Cannot have ASCII control characters, space, tilde, caret, colon,
|
|
104
|
+
// question mark, asterisk, open bracket, or backslash
|
|
105
|
+
if (/[\x00-\x1f\x7f ~^:?*\[\\]/.test(name)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Cannot start or end with a slash, or have consecutive slashes
|
|
110
|
+
if (name.startsWith('/') || name.endsWith('/') || name.includes('//')) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Cannot end with a dot
|
|
115
|
+
if (name.endsWith('.')) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Cannot contain @{
|
|
120
|
+
if (name.includes('@{')) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Cannot be exactly @
|
|
125
|
+
if (name === '@') {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Cannot end with .lock
|
|
130
|
+
if (name.endsWith('.lock')) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Cannot start with a dash
|
|
135
|
+
if (name.startsWith('-')) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Each component cannot start with a dot
|
|
140
|
+
const components = name.split('/');
|
|
141
|
+
for (const component of components) {
|
|
142
|
+
if (component.startsWith('.') || component.length === 0) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
86
150
|
/**
|
|
87
151
|
* Extract repository name from owner/repo format
|
|
88
152
|
*
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tracks whether setup commands have been run for a workspace
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, writeFileSync } from 'fs';
|
|
6
|
+
import { existsSync, writeFileSync, unlinkSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { SpacesError } from '../types/errors.js';
|
|
9
9
|
|
|
@@ -45,3 +45,19 @@ export function markSetupComplete(workspacePath: string): void {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clear the setup marker for a workspace (for testing)
|
|
51
|
+
* @param workspacePath Absolute path to the workspace directory
|
|
52
|
+
*/
|
|
53
|
+
export function clearSetupMarker(workspacePath: string): void {
|
|
54
|
+
const markerPath = join(workspacePath, SETUP_MARKER_FILE);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
if (existsSync(markerPath)) {
|
|
58
|
+
unlinkSync(markerPath);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore errors - this is just for testing
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(bun test:*)",
|
|
5
|
-
"Bash(xargs ls:*)",
|
|
6
|
-
"Bash(bun pm:*)",
|
|
7
|
-
"Bash(node -e:*)",
|
|
8
|
-
"Bash(bun run typecheck:*)",
|
|
9
|
-
"Bash(bun -e:*)",
|
|
10
|
-
"Bash(bun:*)",
|
|
11
|
-
"Bash(xargs:*)",
|
|
12
|
-
"Bash(timeout 2 bash -c 'echo \"\"invalid-base64\"\" | bun src/index.ts access add \"\"not-valid-base64-data\"\" 2>&1')",
|
|
13
|
-
"Bash(wc:*)",
|
|
14
|
-
"WebFetch(domain:ghostty.org)",
|
|
15
|
-
"Bash(xxd:*)",
|
|
16
|
-
"WebFetch(domain:ast-grep.github.io)",
|
|
17
|
-
"WebFetch(domain:developers.cloudflare.com)",
|
|
18
|
-
"WebFetch(domain:gitspace.sh)",
|
|
19
|
-
"Bash(/Users/bradleat/spaces/spaces/workspaces/remote-space/dist/gssh-darwin-arm64:*)",
|
|
20
|
-
"Bash(npx wrangler pages deploy:*)",
|
|
21
|
-
"Bash(git add:*)",
|
|
22
|
-
"Bash(git commit:*)"
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|