gitspace 0.2.0-rc.16 → 0.2.0-rc.18
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/.gitspace/{select → scripts/select}/01-status.sh +6 -6
- package/README.md +6 -7
- package/bun.lock +5 -5
- package/docs/SITE_DOCS_FIGMA_MAKE.md +7 -8
- package/landing-page/src/components/docs/DocsContent.tsx +2 -2
- package/package.json +5 -5
- package/src/app/session/useWorkspaceDeleteFlow.ts +22 -8
- package/src/app.tui.tsx +16 -3
- package/src/app.web.tsx +3 -2
- package/src/commands/migrate.ts +52 -0
- package/src/components/RemoteMachineScreen.tui.tsx +32 -2
- package/src/core/__tests__/bundle.test.ts +209 -0
- package/src/core/bundle.ts +70 -0
- package/src/core/secret-runtime.ts +90 -28
- package/src/core/workspace.ts +23 -3
- package/src/index.ts +22 -0
- package/src/session/useBundleRefreshAttachFlow.ts +6 -12
- package/src/utils/__tests__/run-scripts.test.ts +106 -5
- package/src/utils/__tests__/workspace-setup.integration.test.ts +26 -0
- package/src/utils/normalize-env-key.ts +13 -0
- package/src/utils/run-scripts.ts +109 -14
- package/src/utils/secrets.ts +489 -121
- /package/.gitspace/{setup → scripts/setup}/01-install-deps.sh +0 -0
- /package/.gitspace/{setup → scripts/setup}/02-typecheck.sh +0 -0
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
#
|
|
4
4
|
# This runs every time you switch to an existing workspace.
|
|
5
5
|
#
|
|
6
|
-
# Bundle values are available
|
|
7
|
-
# Example:
|
|
6
|
+
# Bundle values are available using exact keys and uppercase snake-case aliases.
|
|
7
|
+
# Example aliases: DEVELOPER_NAME, EXAMPLE_API_TOKEN
|
|
8
8
|
|
|
9
9
|
WORKSPACE_NAME=$1
|
|
10
10
|
REPOSITORY=$2
|
|
@@ -14,14 +14,14 @@ echo "=== Workspace: $WORKSPACE_NAME ==="
|
|
|
14
14
|
echo ""
|
|
15
15
|
|
|
16
16
|
# Show bundle values (proof of concept)
|
|
17
|
-
if [ -n "$
|
|
18
|
-
echo "Welcome back, $
|
|
17
|
+
if [ -n "$DEVELOPER_NAME" ]; then
|
|
18
|
+
echo "Welcome back, $DEVELOPER_NAME!"
|
|
19
19
|
fi
|
|
20
20
|
|
|
21
21
|
# Show that we have access to the secret (masked)
|
|
22
|
-
if [ -n "$
|
|
22
|
+
if [ -n "$EXAMPLE_API_TOKEN" ]; then
|
|
23
23
|
# Only show first 4 characters to prove we have access
|
|
24
|
-
TOKEN_PREVIEW="${
|
|
24
|
+
TOKEN_PREVIEW="${EXAMPLE_API_TOKEN:0:4}..."
|
|
25
25
|
echo "API Token available: $TOKEN_PREVIEW (stored in OS keychain)"
|
|
26
26
|
fi
|
|
27
27
|
|
package/README.md
CHANGED
|
@@ -183,7 +183,7 @@ A bundle is a directory (typically `.gitspace/`) containing:
|
|
|
183
183
|
Bundle values are passed to scripts as environment variables using the configured bundle keys:
|
|
184
184
|
|
|
185
185
|
- `<KEY>` - Regular or secret value using the exact `configKey` from `bundle.json`
|
|
186
|
-
-
|
|
186
|
+
- `<NORMALIZED_KEY>` - Uppercase snake-case alias (for example, `teamName` -> `TEAM_NAME`)
|
|
187
187
|
|
|
188
188
|
**Example script:**
|
|
189
189
|
|
|
@@ -195,12 +195,12 @@ WORKSPACE_NAME=$1
|
|
|
195
195
|
REPOSITORY=$2
|
|
196
196
|
|
|
197
197
|
# Access bundle values
|
|
198
|
-
if [ -n "$
|
|
199
|
-
echo "Welcome, $
|
|
198
|
+
if [ -n "$TEAM_NAME" ]; then
|
|
199
|
+
echo "Welcome, $TEAM_NAME team!"
|
|
200
200
|
fi
|
|
201
201
|
|
|
202
202
|
# Access secrets (stored securely in OS keychain)
|
|
203
|
-
if [ -n "$
|
|
203
|
+
if [ -n "$API_KEY" ]; then
|
|
204
204
|
echo "API Key configured"
|
|
205
205
|
fi
|
|
206
206
|
```
|
|
@@ -393,9 +393,8 @@ inside each workspace so they can vary by branch:
|
|
|
393
393
|
export SPACES_CURRENT_PROJECT="my-app"
|
|
394
394
|
|
|
395
395
|
# Available in scripts (from bundle onboarding):
|
|
396
|
-
# <KEY> - Value by bundle config key name
|
|
397
|
-
#
|
|
398
|
-
# SPACE_SECRET_<KEY> - Legacy alias for secret values
|
|
396
|
+
# <KEY> - Value by exact bundle config key name
|
|
397
|
+
# <NORMALIZED_KEY> - Uppercase snake-case alias (e.g. teamName -> TEAM_NAME)
|
|
399
398
|
```
|
|
400
399
|
|
|
401
400
|
## Directory Structure
|
package/bun.lock
CHANGED
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"typescript": "^5.9.3",
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@gitspace/darwin-arm64": "0.2.0-rc.
|
|
43
|
-
"@gitspace/darwin-x64": "0.2.0-rc.
|
|
44
|
-
"@gitspace/linux-arm64": "0.2.0-rc.
|
|
45
|
-
"@gitspace/linux-x64": "0.2.0-rc.
|
|
42
|
+
"@gitspace/darwin-arm64": "0.2.0-rc.17",
|
|
43
|
+
"@gitspace/darwin-x64": "0.2.0-rc.17",
|
|
44
|
+
"@gitspace/linux-arm64": "0.2.0-rc.17",
|
|
45
|
+
"@gitspace/linux-x64": "0.2.0-rc.17",
|
|
46
46
|
},
|
|
47
47
|
},
|
|
48
48
|
},
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
|
|
64
64
|
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
|
|
65
65
|
|
|
66
|
-
"@gitspace/darwin-arm64": ["@gitspace/darwin-arm64@0.2.0-rc.
|
|
66
|
+
"@gitspace/darwin-arm64": ["@gitspace/darwin-arm64@0.2.0-rc.17", "", { "os": "darwin", "cpu": "arm64", "bin": { "gssh-darwin-arm64": "bin/gssh" } }, "sha512-aPei17D31di296UBdqOt59HrdvcIRL2oVGVRWT4etCIESgjTyhZQcCLsIezsWI4E4/9DQAqaLAfTif7TsuCRHQ=="],
|
|
67
67
|
|
|
68
68
|
"@graphql-typed-document-node/core": ["@graphql-typed-document-node/core@3.2.0", "", { "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ=="],
|
|
69
69
|
|
|
@@ -377,15 +377,14 @@ Using bundle values in scripts:
|
|
|
377
377
|
```bash
|
|
378
378
|
#!/bin/bash
|
|
379
379
|
# Values available as environment variables:
|
|
380
|
-
# <KEY> - Value by bundle config key name
|
|
381
|
-
#
|
|
382
|
-
# SPACE_SECRET_<KEY> - Legacy alias for secret values
|
|
380
|
+
# <KEY> - Value by exact bundle config key name
|
|
381
|
+
# <NORMALIZED_KEY> - Uppercase snake-case alias (e.g. teamName -> TEAM_NAME)
|
|
383
382
|
|
|
384
|
-
if [ -n "$
|
|
385
|
-
echo "Welcome, $
|
|
383
|
+
if [ -n "$TEAM_NAME" ]; then
|
|
384
|
+
echo "Welcome, $TEAM_NAME team!"
|
|
386
385
|
fi
|
|
387
386
|
|
|
388
|
-
if [ -n "$
|
|
387
|
+
if [ -n "$API_KEY" ]; then
|
|
389
388
|
echo "API Key configured"
|
|
390
389
|
fi
|
|
391
390
|
```
|
|
@@ -960,8 +959,8 @@ Bundles allow teams to share onboarding configurations. Place in `.gitspace/`:
|
|
|
960
959
|
|
|
961
960
|
**Using values in scripts:**
|
|
962
961
|
```bash
|
|
963
|
-
echo "Team: $
|
|
964
|
-
echo "Has API key: $
|
|
962
|
+
echo "Team: $TEAM_NAME"
|
|
963
|
+
echo "Has API key: $API_KEY"
|
|
965
964
|
```
|
|
966
965
|
|
|
967
966
|
---
|
|
@@ -378,8 +378,8 @@ git status`} />
|
|
|
378
378
|
</div>
|
|
379
379
|
|
|
380
380
|
<h3 className="text-xl font-semibold text-white mb-4">Using Values in Scripts</h3>
|
|
381
|
-
<JsonBlock code={`echo "Team: $
|
|
382
|
-
echo "Has API key: $
|
|
381
|
+
<JsonBlock code={`echo "Team: $TEAM_NAME"
|
|
382
|
+
echo "Has API key: $API_KEY"`} />
|
|
383
383
|
</div>
|
|
384
384
|
);
|
|
385
385
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitspace",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.18",
|
|
4
4
|
"description": "CLI for managing GitHub workspaces with git worktrees and secure remote terminal access",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gssh": "./bin/gssh"
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"relay": "bun src/relay/index.ts"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@gitspace/darwin-arm64": "0.2.0-rc.
|
|
21
|
-
"@gitspace/darwin-x64": "0.2.0-rc.
|
|
22
|
-
"@gitspace/linux-x64": "0.2.0-rc.
|
|
23
|
-
"@gitspace/linux-arm64": "0.2.0-rc.
|
|
20
|
+
"@gitspace/darwin-arm64": "0.2.0-rc.18",
|
|
21
|
+
"@gitspace/darwin-x64": "0.2.0-rc.18",
|
|
22
|
+
"@gitspace/linux-x64": "0.2.0-rc.18",
|
|
23
|
+
"@gitspace/linux-arm64": "0.2.0-rc.18"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"cli",
|
|
@@ -30,6 +30,7 @@ export interface UseWorkspaceDeleteFlowOptions {
|
|
|
30
30
|
onDeleteSuccess?: (context: WorkspaceDeleteContext) => void | Promise<void>;
|
|
31
31
|
onDeleteCancelled?: (context: WorkspaceDeleteContext) => void | Promise<void>;
|
|
32
32
|
onDeleteError?: (context: WorkspaceDeleteErrorContext) => void | Promise<void>;
|
|
33
|
+
showLoadingDuringDelete?: boolean;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
export interface UseWorkspaceDeleteFlowResult {
|
|
@@ -86,6 +87,7 @@ export function useWorkspaceDeleteFlow(
|
|
|
86
87
|
onDeleteSuccess,
|
|
87
88
|
onDeleteCancelled,
|
|
88
89
|
onDeleteError,
|
|
90
|
+
showLoadingDuringDelete = false,
|
|
89
91
|
} = options;
|
|
90
92
|
|
|
91
93
|
const executeDelete = useCallback(async (
|
|
@@ -100,13 +102,17 @@ export function useWorkspaceDeleteFlow(
|
|
|
100
102
|
};
|
|
101
103
|
|
|
102
104
|
await onBeforeDelete?.(context);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
if (showLoadingDuringDelete) {
|
|
106
|
+
flow.showLoading({
|
|
107
|
+
title: 'Deleting Workspace',
|
|
108
|
+
message:
|
|
109
|
+
params.scriptPolicy === 'skip'
|
|
110
|
+
? 'Removing workspace without cleanup scripts...'
|
|
111
|
+
: `Running cleanup scripts for "${target.workspaceName}"...`,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
flow.close();
|
|
115
|
+
}
|
|
110
116
|
|
|
111
117
|
try {
|
|
112
118
|
await deleteWorkspace(target.projectName, target.workspaceId, params);
|
|
@@ -144,7 +150,15 @@ export function useWorkspaceDeleteFlow(
|
|
|
144
150
|
|
|
145
151
|
return false;
|
|
146
152
|
}
|
|
147
|
-
}, [
|
|
153
|
+
}, [
|
|
154
|
+
deleteWorkspace,
|
|
155
|
+
flow,
|
|
156
|
+
onBeforeDelete,
|
|
157
|
+
onDeleteCancelled,
|
|
158
|
+
onDeleteError,
|
|
159
|
+
onDeleteSuccess,
|
|
160
|
+
showLoadingDuringDelete,
|
|
161
|
+
]);
|
|
148
162
|
|
|
149
163
|
const deleteWorkspaceWithPrompt = useCallback(async (target: WorkspaceDeleteTarget): Promise<boolean> => {
|
|
150
164
|
return executeDelete(target, { scriptPolicy: 'auto' }, false);
|
package/src/app.tui.tsx
CHANGED
|
@@ -87,7 +87,10 @@ import { useUserActivity } from './hooks/index.js';
|
|
|
87
87
|
import { useBundleRefreshAttachFlow } from './session/index.js';
|
|
88
88
|
import { useAttachController } from './app/session/useAttachController.js';
|
|
89
89
|
import { useWorkspaceDeleteFlow } from './app/session/useWorkspaceDeleteFlow.js';
|
|
90
|
-
import {
|
|
90
|
+
import {
|
|
91
|
+
consumeLegacyCleanupReminderForTui,
|
|
92
|
+
initializeSecretRuntime,
|
|
93
|
+
} from './core/secret-runtime.js';
|
|
91
94
|
import {
|
|
92
95
|
resolveInboxCommand,
|
|
93
96
|
resolveMachineListCommand,
|
|
@@ -1322,9 +1325,13 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
1322
1325
|
// ========== Keyboard Handlers ==========
|
|
1323
1326
|
|
|
1324
1327
|
useKeyboard(async (key) => {
|
|
1328
|
+
const localScriptTerminalRunning =
|
|
1329
|
+
state.view === 'scripts' &&
|
|
1330
|
+
(localScriptState?.isRunning ?? true);
|
|
1331
|
+
|
|
1325
1332
|
// Handle flow modals FIRST - even in terminal view
|
|
1326
1333
|
// This ensures y/n work in confirmation modals when terminal is underneath
|
|
1327
|
-
if (flow.isOpen) {
|
|
1334
|
+
if (flow.isOpen && !localScriptTerminalRunning) {
|
|
1328
1335
|
// Handle confirm modal with y/n shortcuts
|
|
1329
1336
|
if (flow.flow.type === 'confirm') {
|
|
1330
1337
|
if (key.raw === 'y' || key.name === 'return') {
|
|
@@ -2059,7 +2066,7 @@ function App({ relayConfig, onQuit }: AppProps) {
|
|
|
2059
2066
|
error={localScriptState?.error}
|
|
2060
2067
|
exitCode={localScriptState?.exitCode}
|
|
2061
2068
|
/>
|
|
2062
|
-
<FlowTUI flow={flow} />
|
|
2069
|
+
{!isRunning && <FlowTUI flow={flow} />}
|
|
2063
2070
|
<StatusBar hint={isRunning ? '[Running scripts...]' : '[Esc/n] Back to workspaces'} />
|
|
2064
2071
|
</Fragment>
|
|
2065
2072
|
);
|
|
@@ -2590,6 +2597,12 @@ export async function launchTUI(
|
|
|
2590
2597
|
// Clean exit handler
|
|
2591
2598
|
const handleQuit = () => {
|
|
2592
2599
|
renderer.destroy();
|
|
2600
|
+
|
|
2601
|
+
const legacyReminder = consumeLegacyCleanupReminderForTui();
|
|
2602
|
+
if (legacyReminder) {
|
|
2603
|
+
logger.warning(legacyReminder);
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2593
2606
|
process.exit(0);
|
|
2594
2607
|
};
|
|
2595
2608
|
|
package/src/app.web.tsx
CHANGED
|
@@ -747,18 +747,19 @@ export default function App() {
|
|
|
747
747
|
terminal.mode === 'browsing' &&
|
|
748
748
|
showScriptTerminal
|
|
749
749
|
) {
|
|
750
|
+
const isRunning = terminal.scriptState?.isRunning ?? true;
|
|
750
751
|
return (
|
|
751
752
|
<>
|
|
752
753
|
<ScriptTerminal
|
|
753
754
|
phase={terminal.scriptState?.phase ?? 'pre'}
|
|
754
755
|
workspaceName={scriptWorkspaceName}
|
|
755
|
-
isRunning={
|
|
756
|
+
isRunning={isRunning}
|
|
756
757
|
error={terminal.scriptState?.error}
|
|
757
758
|
exitCode={terminal.scriptState?.exitCode}
|
|
758
759
|
setWriteCallback={terminal.setWriteCallback}
|
|
759
760
|
onBack={() => setShowScriptTerminal(false)}
|
|
760
761
|
/>
|
|
761
|
-
<FlowWeb flow={flow} />
|
|
762
|
+
{!isRunning && <FlowWeb flow={flow} />}
|
|
762
763
|
<Toaster theme="dark" position="top-right" richColors />
|
|
763
764
|
</>
|
|
764
765
|
);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getSecretMigrationInputs } from '../core/secret-runtime.js';
|
|
2
|
+
import { logger } from '../utils/logger.js';
|
|
3
|
+
import { promptConfirm } from '../utils/prompts.js';
|
|
4
|
+
import { cleanupLegacySecretEntries, preloadAllSecrets } from '../utils/secrets.js';
|
|
5
|
+
|
|
6
|
+
export interface CleanupLegacyOptions {
|
|
7
|
+
yes?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function migrateCleanupLegacy(
|
|
11
|
+
options: CleanupLegacyOptions = {}
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const migrationInputs = getSecretMigrationInputs();
|
|
14
|
+
|
|
15
|
+
if (!options.yes) {
|
|
16
|
+
const confirmed = await promptConfirm(
|
|
17
|
+
'Delete legacy keychain entries (project:*, global, and old per-key entries)? Unified secrets are kept.',
|
|
18
|
+
false
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (!confirmed) {
|
|
22
|
+
logger.info('Cancelled');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Ensure legacy entries are copied into unified storage before cleanup.
|
|
28
|
+
await preloadAllSecrets(migrationInputs.projectNames, {
|
|
29
|
+
projectLegacyKeys: migrationInputs.projectSecretKeys,
|
|
30
|
+
globalLegacyKeys: migrationInputs.globalSecretKeys,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const result = await cleanupLegacySecretEntries(migrationInputs.projectNames, {
|
|
34
|
+
projectLegacyKeys: migrationInputs.projectSecretKeys,
|
|
35
|
+
globalLegacyKeys: migrationInputs.globalSecretKeys,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (result.errors.length > 0) {
|
|
39
|
+
logger.warning(`Legacy cleanup completed with ${result.errors.length} error(s).`);
|
|
40
|
+
for (const error of result.errors) {
|
|
41
|
+
logger.error(` ${error}`);
|
|
42
|
+
}
|
|
43
|
+
logger.info(
|
|
44
|
+
`Legacy cleanup finished with issues. Deleted ${result.deleted} entries (${result.missing} already absent).`
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.success(
|
|
50
|
+
`Legacy cleanup complete. Deleted ${result.deleted} entries (${result.missing} already absent).`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -89,6 +89,32 @@ export function RemoteMachineScreen({ machine, relayUrl, identity, onBack }: Rem
|
|
|
89
89
|
}
|
|
90
90
|
return remote.selectedProjectName;
|
|
91
91
|
},
|
|
92
|
+
onBeforeAttach: ({ target, params }) => {
|
|
93
|
+
if (target === 'workspace' && params.workspaceId) {
|
|
94
|
+
setShowInbox(false);
|
|
95
|
+
setScriptWorkspaceName(params.workspaceId.split(':').slice(-1)[0] ?? params.workspaceId);
|
|
96
|
+
setShowScriptTerminal(true);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
onAttachCancelled: ({ target }) => {
|
|
100
|
+
if (target === 'workspace') {
|
|
101
|
+
setShowScriptTerminal(false);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
onAttachError: ({ target, message }) => {
|
|
105
|
+
const isWorkspaceScriptFailure = message.startsWith('Workspace scripts failed during');
|
|
106
|
+
const hasScriptRuntimeState = Boolean(remote.scriptState);
|
|
107
|
+
|
|
108
|
+
if (target === 'workspace' && (!isWorkspaceScriptFailure || !hasScriptRuntimeState)) {
|
|
109
|
+
setShowScriptTerminal(false);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
flow.showMessage({
|
|
113
|
+
title: isWorkspaceScriptFailure ? 'Workspace Script Failed' : 'Session Failed',
|
|
114
|
+
message,
|
|
115
|
+
variant: 'error',
|
|
116
|
+
});
|
|
117
|
+
},
|
|
92
118
|
});
|
|
93
119
|
|
|
94
120
|
const { deleteWorkspaceWithPrompt } = useWorkspaceDeleteFlow({
|
|
@@ -221,7 +247,11 @@ export function RemoteMachineScreen({ machine, relayUrl, identity, onBack }: Rem
|
|
|
221
247
|
}, [flow, renderer]);
|
|
222
248
|
|
|
223
249
|
useKeyboard(async (key) => {
|
|
224
|
-
|
|
250
|
+
const scriptTerminalRunning =
|
|
251
|
+
showScriptTerminal &&
|
|
252
|
+
(remote.scriptState?.isRunning ?? true);
|
|
253
|
+
|
|
254
|
+
if (flow.isOpen && !scriptTerminalRunning) {
|
|
225
255
|
if (flow.flow.type === 'confirm') {
|
|
226
256
|
if (key.raw === 'y' || key.name === 'return') {
|
|
227
257
|
await flow.handleConfirm();
|
|
@@ -429,7 +459,7 @@ export function RemoteMachineScreen({ machine, relayUrl, identity, onBack }: Rem
|
|
|
429
459
|
error={remote.scriptState?.error}
|
|
430
460
|
exitCode={remote.scriptState?.exitCode}
|
|
431
461
|
/>
|
|
432
|
-
<FlowTUI flow={flow} />
|
|
462
|
+
{!isRunning && <FlowTUI flow={flow} />}
|
|
433
463
|
<StatusBar hint={isRunning ? '[Running scripts...]' : '[Esc/n] Back to workspaces'} />
|
|
434
464
|
</Fragment>
|
|
435
465
|
);
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { validateBundle } from '../bundle';
|
|
3
|
+
|
|
4
|
+
describe('validateBundle', () => {
|
|
5
|
+
it('allows unique config keys with distinct normalized aliases', () => {
|
|
6
|
+
expect(() =>
|
|
7
|
+
validateBundle({
|
|
8
|
+
version: '1.0',
|
|
9
|
+
name: 'Valid Bundle',
|
|
10
|
+
onboarding: [
|
|
11
|
+
{
|
|
12
|
+
id: 'env-name',
|
|
13
|
+
type: 'input',
|
|
14
|
+
title: 'Environment Name',
|
|
15
|
+
description: 'Name of environment',
|
|
16
|
+
configKey: 'vercelEnv',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'api-token',
|
|
20
|
+
type: 'secret',
|
|
21
|
+
title: 'API Token',
|
|
22
|
+
description: 'Service token',
|
|
23
|
+
configKey: 'apiToken',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
})
|
|
27
|
+
).not.toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('throws helpful error when configKey is duplicated across steps', () => {
|
|
31
|
+
expect(() =>
|
|
32
|
+
validateBundle({
|
|
33
|
+
version: '1.0',
|
|
34
|
+
name: 'Duplicate Key Bundle',
|
|
35
|
+
onboarding: [
|
|
36
|
+
{
|
|
37
|
+
id: 'api-token-input',
|
|
38
|
+
type: 'input',
|
|
39
|
+
title: 'API token input',
|
|
40
|
+
description: 'input',
|
|
41
|
+
configKey: 'apiToken',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'api-token-secret',
|
|
45
|
+
type: 'secret',
|
|
46
|
+
title: 'API token secret',
|
|
47
|
+
description: 'secret',
|
|
48
|
+
configKey: 'apiToken',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
})
|
|
52
|
+
).toThrow(/Bundle configKey collision/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('throws helpful error when normalized aliases collide', () => {
|
|
56
|
+
expect(() =>
|
|
57
|
+
validateBundle({
|
|
58
|
+
version: '1.0',
|
|
59
|
+
name: 'Alias Collision Bundle',
|
|
60
|
+
onboarding: [
|
|
61
|
+
{
|
|
62
|
+
id: 'api-token-dash',
|
|
63
|
+
type: 'input',
|
|
64
|
+
title: 'API token dash',
|
|
65
|
+
description: 'dash',
|
|
66
|
+
configKey: 'api-token',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'api-token-camel',
|
|
70
|
+
type: 'secret',
|
|
71
|
+
title: 'API token camel',
|
|
72
|
+
description: 'camel',
|
|
73
|
+
configKey: 'apiToken',
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
).toThrow(/Bundle configKey alias collision/);
|
|
78
|
+
|
|
79
|
+
expect(() =>
|
|
80
|
+
validateBundle({
|
|
81
|
+
version: '1.0',
|
|
82
|
+
name: 'Alias Collision Bundle',
|
|
83
|
+
onboarding: [
|
|
84
|
+
{
|
|
85
|
+
id: 'api-token-dash',
|
|
86
|
+
type: 'input',
|
|
87
|
+
title: 'API token dash',
|
|
88
|
+
description: 'dash',
|
|
89
|
+
configKey: 'api-token',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'api-token-camel',
|
|
93
|
+
type: 'secret',
|
|
94
|
+
title: 'API token camel',
|
|
95
|
+
description: 'camel',
|
|
96
|
+
configKey: 'apiToken',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
})
|
|
100
|
+
).toThrow(/API_TOKEN/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('treats acronym boundaries as part of normalized alias collisions', () => {
|
|
104
|
+
expect(() =>
|
|
105
|
+
validateBundle({
|
|
106
|
+
version: '1.0',
|
|
107
|
+
name: 'Acronym Collision Bundle',
|
|
108
|
+
onboarding: [
|
|
109
|
+
{
|
|
110
|
+
id: 'http-client-acronym',
|
|
111
|
+
type: 'input',
|
|
112
|
+
title: 'HTTP client acronym',
|
|
113
|
+
description: 'acronym',
|
|
114
|
+
configKey: 'myHTTPClient',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'http-client-camel',
|
|
118
|
+
type: 'secret',
|
|
119
|
+
title: 'HTTP client camel',
|
|
120
|
+
description: 'camel',
|
|
121
|
+
configKey: 'myHttpClient',
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
})
|
|
125
|
+
).toThrow(/Bundle configKey alias collision/);
|
|
126
|
+
|
|
127
|
+
expect(() =>
|
|
128
|
+
validateBundle({
|
|
129
|
+
version: '1.0',
|
|
130
|
+
name: 'Acronym Collision Bundle',
|
|
131
|
+
onboarding: [
|
|
132
|
+
{
|
|
133
|
+
id: 'http-client-acronym',
|
|
134
|
+
type: 'input',
|
|
135
|
+
title: 'HTTP client acronym',
|
|
136
|
+
description: 'acronym',
|
|
137
|
+
configKey: 'myHTTPClient',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'http-client-camel',
|
|
141
|
+
type: 'secret',
|
|
142
|
+
title: 'HTTP client camel',
|
|
143
|
+
description: 'camel',
|
|
144
|
+
configKey: 'myHttpClient',
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
})
|
|
148
|
+
).toThrow(/MY_HTTP_CLIENT/);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('throws helpful error when normalized alias is not shell-safe', () => {
|
|
152
|
+
expect(() =>
|
|
153
|
+
validateBundle({
|
|
154
|
+
version: '1.0',
|
|
155
|
+
name: 'Digit Prefix Bundle',
|
|
156
|
+
onboarding: [
|
|
157
|
+
{
|
|
158
|
+
id: 'two-fa-token',
|
|
159
|
+
type: 'secret',
|
|
160
|
+
title: '2FA token',
|
|
161
|
+
description: '2FA token',
|
|
162
|
+
configKey: '2faToken',
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
})
|
|
166
|
+
).toThrow(/non-shell env alias/);
|
|
167
|
+
|
|
168
|
+
expect(() =>
|
|
169
|
+
validateBundle({
|
|
170
|
+
version: '1.0',
|
|
171
|
+
name: 'Digit Prefix Bundle',
|
|
172
|
+
onboarding: [
|
|
173
|
+
{
|
|
174
|
+
id: 'two-fa-token',
|
|
175
|
+
type: 'secret',
|
|
176
|
+
title: '2FA token',
|
|
177
|
+
description: '2FA token',
|
|
178
|
+
configKey: '2faToken',
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
})
|
|
182
|
+
).toThrow(/2FA_TOKEN/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('detects exact key and normalized alias collisions', () => {
|
|
186
|
+
expect(() =>
|
|
187
|
+
validateBundle({
|
|
188
|
+
version: '1.0',
|
|
189
|
+
name: 'Exact Alias Collision Bundle',
|
|
190
|
+
onboarding: [
|
|
191
|
+
{
|
|
192
|
+
id: 'exact-upper',
|
|
193
|
+
type: 'input',
|
|
194
|
+
title: 'Exact upper',
|
|
195
|
+
description: 'exact',
|
|
196
|
+
configKey: 'API_TOKEN',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: 'camel-key',
|
|
200
|
+
type: 'secret',
|
|
201
|
+
title: 'Camel key',
|
|
202
|
+
description: 'camel',
|
|
203
|
+
configKey: 'apiToken',
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
})
|
|
207
|
+
).toThrow(/Bundle configKey alias collision/);
|
|
208
|
+
});
|
|
209
|
+
});
|