@capgo/cli 8.8.1 → 8.9.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.
- package/dist/index.js +816 -629
- package/dist/package.json +13 -2
- package/dist/src/build/onboarding/android/flow.d.ts +19 -0
- package/dist/src/build/onboarding/android/types.d.ts +8 -0
- package/dist/src/build/onboarding/apple-api.d.ts +2 -1
- package/dist/src/build/onboarding/ios/flow.d.ts +22 -0
- package/dist/src/build/onboarding/macos-signing.d.ts +13 -3
- package/dist/src/build/onboarding/mcp/app-id-validation.d.ts +1 -1
- package/dist/src/build/onboarding/mcp/broker-oauth.d.ts +30 -0
- package/dist/src/build/onboarding/mcp/broker-session.d.ts +19 -0
- package/dist/src/build/onboarding/mcp/build-job.d.ts +94 -0
- package/dist/src/build/onboarding/mcp/build-tools.d.ts +25 -0
- package/dist/src/build/onboarding/mcp/credentials-manage.d.ts +79 -0
- package/dist/src/build/onboarding/mcp/engine.d.ts +152 -35
- package/dist/src/build/onboarding/mcp/onboarding-tools.d.ts +17 -1
- package/dist/src/build/onboarding/mcp/session-state.d.ts +160 -0
- package/dist/src/build/onboarding/mcp/step-input.d.ts +49 -2
- package/dist/src/build/onboarding/mcp/tail-progress.d.ts +6 -0
- package/dist/src/build/onboarding/types.d.ts +1 -1
- package/dist/src/build/output-record.d.ts +43 -4
- package/dist/src/mcp/instructions.d.ts +15 -0
- package/dist/src/mcp/stdout-guard.d.ts +19 -0
- package/dist/src/schemas/onboarding.d.ts +100 -0
- package/dist/src/sdk.d.ts +8 -0
- package/dist/src/sdk.js +218 -218
- package/dist/src/support/contact-support.d.ts +1 -0
- package/dist/src/utils/safeWrites.d.ts +5 -0
- package/package.json +13 -2
- package/dist/src/build/onboarding/mcp/terminal-launch.d.ts +0 -22
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { AndroidOnboardingProgress } from '../android/types.js';
|
|
1
|
+
import type { AndroidOnboardingProgress, AndroidOnboardingStep } from '../android/types.js';
|
|
2
2
|
import type { AndroidEffectDeps } from '../android/flow.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { IosEffectDeps, IosStepCtx, IosStepView } from '../ios/flow.js';
|
|
4
|
+
import type { OnboardingProgress, OnboardingStep } from '../types.js';
|
|
4
5
|
import type { NextStepResult, Platform } from './contract.js';
|
|
5
6
|
import type { BuildOutputRecord } from '../../output-record.js';
|
|
6
7
|
import { androidViewForStep } from '../android/flow.js';
|
|
7
|
-
import {
|
|
8
|
+
import { brokerBegin, brokerClear, brokerPoll } from './broker-session.js';
|
|
8
9
|
/** Facts gathered during preflight; the pure deciders branch only on these. */
|
|
9
10
|
export interface PreflightFacts {
|
|
10
11
|
capacitorProject: boolean;
|
|
@@ -30,6 +31,14 @@ interface OnboardingInput {
|
|
|
30
31
|
gcpProjectName?: string;
|
|
31
32
|
androidPackage?: string;
|
|
32
33
|
saMethodChoice?: 'retry' | 'save-anyway' | 'oauth';
|
|
34
|
+
/** Set true at google-sign-in to (re)open the browser for a fresh OAuth — recovery when the browser didn't open, was closed, or the sign-in stalled. */
|
|
35
|
+
reopenSignIn?: boolean;
|
|
36
|
+
/** At google-sign-in: open the broker sign-in link in the user's browser (true) or let them open it (false/omit). */
|
|
37
|
+
openSignInBrowser?: boolean;
|
|
38
|
+
/** The confirmation code the user reads off the broker success page — releases the token on poll. */
|
|
39
|
+
confirmCode?: string;
|
|
40
|
+
/** Answer to the resume prompt: 'continue' resumes the saved step, 'restart' wipes this platform's saved progress and begins again. */
|
|
41
|
+
resumeChoice?: 'continue' | 'restart';
|
|
33
42
|
credentialsExistChoice?: 'backup' | 'cancel';
|
|
34
43
|
keystoreMethod?: 'existing' | 'generate';
|
|
35
44
|
keystorePath?: string;
|
|
@@ -39,16 +48,110 @@ interface OnboardingInput {
|
|
|
39
48
|
keystoreNewAlias?: string;
|
|
40
49
|
keystorePasswordMethod?: 'random' | 'manual';
|
|
41
50
|
keystoreCommonName?: string;
|
|
51
|
+
/** Answer to the parked iOS verify-app gate (the TUI Select vocabulary). */
|
|
52
|
+
verifyAction?: 'pick' | 'create-new' | 'autofix' | 'continue' | 'recheck' | 'open' | 'reopen' | 'back' | 'cancel';
|
|
53
|
+
/** The picked App Store app's bundle id — only with verifyAction 'pick'. */
|
|
54
|
+
verifyAppId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Answer to the parked iOS cert-limit-prompt (S6b): the Apple resource id of
|
|
57
|
+
* the Distribution certificate to revoke, or '__exit__' (the engine's
|
|
58
|
+
* OPTION_CERT_LIMIT_EXIT sentinel) to stop.
|
|
59
|
+
*/
|
|
60
|
+
certToRevoke?: string;
|
|
61
|
+
/** Answer to the parked iOS duplicate-profile-prompt (S6b). */
|
|
62
|
+
duplicateProfileAction?: 'delete' | 'exit';
|
|
63
|
+
/**
|
|
64
|
+
* Answer to the parked iOS error recovery screen (S6b): 'retry' re-runs the
|
|
65
|
+
* failing step (carried.retryStep), 'restart' wipes progress + session and
|
|
66
|
+
* starts over, 'exit' stops, 'email-support' surfaces support instructions
|
|
67
|
+
* (MCP-only arm — no host-side opens).
|
|
68
|
+
*/
|
|
69
|
+
errorAction?: 'retry' | 'restart' | 'exit' | 'email-support';
|
|
70
|
+
/** Answer to the CI-secrets choice steps (target-select / ask / overwrite / push-confirm / setup / failed). */
|
|
71
|
+
ciSecretAction?: 'github' | 'gitlab' | 'skip' | 'yes' | 'no' | 'replace' | 'retry' | 'continue' | 'confirm' | 'cancel';
|
|
72
|
+
/** Answer to ask-github-actions-setup ('no' maps to the persisted setupMode 'declined'). */
|
|
73
|
+
githubActionsSetup?: 'with-workflow' | 'secrets-only' | 'no';
|
|
74
|
+
/** Answer to ask-export-env (yes/no) and confirm-env-export-overwrite (replace/skip). */
|
|
75
|
+
exportEnvAction?: 'yes' | 'no' | 'replace' | 'skip';
|
|
76
|
+
/** Custom .env target path — only together with exportEnvAction 'yes'. */
|
|
77
|
+
envExportPath?: string;
|
|
78
|
+
/** Answer to pick-package-manager. */
|
|
79
|
+
packageManager?: 'bun' | 'npm' | 'pnpm' | 'yarn';
|
|
80
|
+
/** Answer to pick-build-script: a script name, '__custom__', or '__skip__'. */
|
|
81
|
+
buildScript?: string;
|
|
82
|
+
/** Answer to pick-build-script-custom: the exact custom build command. */
|
|
83
|
+
buildScriptCustom?: string;
|
|
84
|
+
/** Answer to preview-workflow-file: write / view (returns the file text, re-asks) / cancel. */
|
|
85
|
+
workflowFileAction?: 'write' | 'view' | 'cancel';
|
|
86
|
+
/** Answer to the iOS setup-method fork: create fresh credentials via Apple, or import from this Mac's Keychain. */
|
|
87
|
+
setupMethod?: 'create-new' | 'import-existing';
|
|
88
|
+
/** Answer to import-distribution-mode ('__cancel__' switches to the create-new path). */
|
|
89
|
+
importDistribution?: 'app_store' | 'ad_hoc' | '__cancel__';
|
|
90
|
+
/** Answer to import-pick-identity: the chosen identity's SHA-1 (an option value), or '__cancel__' for create-new. */
|
|
91
|
+
identityChoice?: string;
|
|
92
|
+
/** Answer to import-pick-profile: the chosen profile's UUID (an option value), or '__back__' to re-pick the identity. */
|
|
93
|
+
profileChoice?: string;
|
|
94
|
+
/** Answer to the import-no-match-recovery hub. */
|
|
95
|
+
importRecoveryAction?: 'create' | 'provide-profile-path' | 'browser' | 'back';
|
|
96
|
+
/** Answer to import-portal-explanation (the manual Apple-portal walkthrough). */
|
|
97
|
+
portalAction?: 'use-create' | 'open-anyway' | 'use-file' | 'back';
|
|
98
|
+
/** Absolute path to a .mobileprovision file — answers import-provide-profile-path (the MCP's manual-path arm of the TUI's native picker). */
|
|
99
|
+
profilePath?: string;
|
|
100
|
+
/** Answer to import-export-warning: 'go' exports from the Keychain (the one macOS permission dialog), 'back' re-picks the profile, 'exit' stops. */
|
|
101
|
+
exportConfirm?: 'go' | 'back' | 'exit';
|
|
42
102
|
}
|
|
43
103
|
/** Decide the first/again step for a fresh or resumed session. */
|
|
44
104
|
export declare function decideStart(facts: PreflightFacts, progress: OnboardingProgress | null, deps: EngineDeps): Promise<NextStepResult>;
|
|
45
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Map an interactive IosStepView into a NextStepResult — the iOS mirror of
|
|
107
|
+
* mapAndroidView. State names reuse the engine step ids; option values mirror
|
|
108
|
+
* the TUI Select values. Only NON-SECRET, view-derived data may appear here.
|
|
109
|
+
*/
|
|
110
|
+
export declare function mapIosView(view: IosStepView, facts: PreflightFacts, ctx?: IosStepCtx): NextStepResult;
|
|
111
|
+
export declare function decideIos(facts: PreflightFacts, deps: EngineDeps, opts?: {
|
|
112
|
+
verifyAction?: OnboardingInput['verifyAction'];
|
|
113
|
+
verifyAppId?: string;
|
|
114
|
+
certToRevoke?: string;
|
|
115
|
+
duplicateProfileAction?: 'delete' | 'exit';
|
|
116
|
+
errorAction?: 'retry' | 'restart' | 'exit' | 'email-support';
|
|
117
|
+
/** import-pick-identity answer: an identity SHA-1 or '__cancel__'. */
|
|
118
|
+
identityChoice?: string;
|
|
119
|
+
/** import-pick-profile answer: a profile UUID or '__back__'. */
|
|
120
|
+
profileChoice?: string;
|
|
121
|
+
/** import-no-match-recovery answer. */
|
|
122
|
+
importRecoveryAction?: 'create' | 'provide-profile-path' | 'browser' | 'back';
|
|
123
|
+
/** import-portal-explanation answer. */
|
|
124
|
+
portalAction?: 'use-create' | 'open-anyway' | 'use-file' | 'back';
|
|
125
|
+
/** import-provide-profile-path answer: the .mobileprovision path. */
|
|
126
|
+
profilePath?: string;
|
|
127
|
+
/** import-export-warning answer. */
|
|
128
|
+
exportConfirm?: 'go' | 'back' | 'exit';
|
|
129
|
+
/**
|
|
130
|
+
* S9-S11: the explicit tail step a validated tail answer routed to
|
|
131
|
+
* (drive() → applyMcpTailAnswer). Honored only while the slim tail
|
|
132
|
+
* progress carries credentialsSaved — the same guard as the tail park.
|
|
133
|
+
*/
|
|
134
|
+
tailNext?: OnboardingStep;
|
|
135
|
+
}): Promise<NextStepResult>;
|
|
46
136
|
export declare function mapAndroidView(view: ReturnType<typeof androidViewForStep>, facts: PreflightFacts, opts?: {
|
|
47
137
|
keystorePath?: string;
|
|
48
138
|
keystorePassword?: string;
|
|
49
139
|
}): NextStepResult;
|
|
50
140
|
export declare function decideAndroid(facts: PreflightFacts, deps: EngineDeps, opts?: {
|
|
51
141
|
signInProceed?: boolean;
|
|
142
|
+
/** Drop any in-flight Google OAuth session and (re)open the browser for a fresh
|
|
143
|
+
* sign-in — recovery for "still waiting" when the browser never opened / was closed. */
|
|
144
|
+
reopenSignIn?: boolean;
|
|
145
|
+
/** At google-sign-in: open the broker sign-in link in the user's browser (vs. letting them open it). */
|
|
146
|
+
openSignInBrowser?: boolean;
|
|
147
|
+
/** The confirmation code the user reads off the broker success page — released the token on the next poll. */
|
|
148
|
+
confirmCode?: string;
|
|
149
|
+
/**
|
|
150
|
+
* S9-S11: the explicit tail step a validated tail answer routed to
|
|
151
|
+
* (drive() → applyMcpTailAnswer). Honored only while the slim tail
|
|
152
|
+
* progress carries credentialsSaved — the same guard as the tail park.
|
|
153
|
+
*/
|
|
154
|
+
tailNext?: AndroidOnboardingStep;
|
|
52
155
|
}): Promise<NextStepResult>;
|
|
53
156
|
export declare function decideAdvance(facts: PreflightFacts, progress: OnboardingProgress | null, input: OnboardingInput | undefined, deps: EngineDeps): Promise<NextStepResult>;
|
|
54
157
|
export interface EngineDeps {
|
|
@@ -66,40 +169,34 @@ export interface EngineDeps {
|
|
|
66
169
|
error: string;
|
|
67
170
|
}>;
|
|
68
171
|
loadAndroidProgress: (appId: string) => Promise<AndroidOnboardingProgress | null>;
|
|
69
|
-
finalizeAndroidCredentials: (appId: string) => Promise<{
|
|
70
|
-
ok: true;
|
|
71
|
-
} | {
|
|
72
|
-
ok: false;
|
|
73
|
-
error: string;
|
|
74
|
-
}>;
|
|
75
172
|
readBuildRecord: (path: string) => Promise<BuildOutputRecord | null>;
|
|
76
173
|
buildRecordPath: (appId: string, platform: Platform) => string;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Remove a build record (and its QR png) left behind by an earlier build.
|
|
176
|
+
* Called by runBuild BEFORE the hand-off so checkBuild can never read a
|
|
177
|
+
* stale record as the new build's result (hostile-review 2026-06-12).
|
|
178
|
+
* Optional so legacy fixtures keep working; production wires
|
|
179
|
+
* removeBuildOutputRecord.
|
|
180
|
+
*/
|
|
181
|
+
clearBuildRecord?: (recordPath: string) => Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* The shared iOS flow engine's IO deps (Apple API / CSR / fs / persistence),
|
|
184
|
+
* pre-bound by the driver (buildIosEffectDeps in onboarding-tools.ts for
|
|
185
|
+
* production; canned fakes in tests). decideIos threads the per-app carried
|
|
186
|
+
* session state in on every effect run. Optional so legacy fixtures that
|
|
187
|
+
* never enter the iOS path keep working — a missing helper inside surfaces
|
|
188
|
+
* as a caught effect error, never a crash.
|
|
189
|
+
*/
|
|
190
|
+
iosEffectDeps?: IosEffectDeps;
|
|
84
191
|
androidEffectDeps: AndroidEffectDeps;
|
|
85
|
-
/** Returns true when the current host can launch Terminal.app (macOS). Injectable for tests. */
|
|
86
|
-
canLaunchTerminal: () => boolean;
|
|
87
|
-
/** Launch `command` in a new macOS Terminal.app window. Injectable for tests. */
|
|
88
|
-
launchBuildInTerminal: (command: string) => Promise<{
|
|
89
|
-
ok: true;
|
|
90
|
-
} | {
|
|
91
|
-
ok: false;
|
|
92
|
-
error: string;
|
|
93
|
-
}>;
|
|
94
192
|
/**
|
|
95
|
-
* Optional injectable OAuth session
|
|
96
|
-
*
|
|
97
|
-
* Production builds omit this and rely on the module-level registry.
|
|
193
|
+
* Optional injectable broker OAuth session for testing. When provided, the engine uses these instead of the
|
|
194
|
+
* disk-persisted broker-session.ts functions. Production omits this and relies on the broker session.
|
|
98
195
|
*/
|
|
99
196
|
oauthSession?: {
|
|
100
|
-
begin: typeof
|
|
101
|
-
poll: typeof
|
|
102
|
-
clear: typeof
|
|
197
|
+
begin: typeof brokerBegin;
|
|
198
|
+
poll: typeof brokerPoll;
|
|
199
|
+
clear: typeof brokerClear;
|
|
103
200
|
};
|
|
104
201
|
/**
|
|
105
202
|
* Write the generated/loaded Android keystore (.p12) to a file on disk so the
|
|
@@ -112,14 +209,34 @@ export interface EngineDeps {
|
|
|
112
209
|
writeKeystoreFile?: (appId: string, base64: string, alias: string) => Promise<string>;
|
|
113
210
|
}
|
|
114
211
|
export declare function gatherFacts(deps: EngineDeps): Promise<PreflightFacts>;
|
|
115
|
-
export declare function runStart(deps: EngineDeps): Promise<NextStepResult>;
|
|
212
|
+
export declare function runStart(deps: EngineDeps, platform?: Platform): Promise<NextStepResult>;
|
|
116
213
|
export declare function runAdvance(deps: EngineDeps, input?: OnboardingInput): Promise<NextStepResult>;
|
|
117
214
|
/**
|
|
118
215
|
* Read-only: determine the onboarding state the user is currently on, WITHOUT
|
|
119
|
-
* running any side effect. Mirrors the branch selection of decideStart/
|
|
120
|
-
* (preflight → platform →
|
|
216
|
+
* running any side effect. Mirrors the branch selection of decideStart/
|
|
217
|
+
* decideAndroid/decideIos (preflight → platform → resume step, with the same
|
|
218
|
+
* ask-build → build-ready / .p8-chain → ios-api-key name mapping) but never
|
|
219
|
+
* calls effects.
|
|
121
220
|
*/
|
|
122
221
|
export declare function resolveCurrentState(facts: PreflightFacts): string;
|
|
222
|
+
/** State names the MCP constructs directly that are NOT engine step ids. */
|
|
223
|
+
export declare const MCP_ONLY_STATES: readonly string[];
|
|
224
|
+
/**
|
|
225
|
+
* Engine step ids (present in the type tables) the MCP can NEVER emit as a
|
|
226
|
+
* state name:
|
|
227
|
+
* - TUI bootstrap / TUI-only screens: welcome, adding-platform,
|
|
228
|
+
* the AI build-debug sub-flow (decideIos reroutes its entry to
|
|
229
|
+
* 'build-failed'), the contact-support sub-flow (the MCP's error screen has
|
|
230
|
+
* the email-support arm instead), the native file pickers (the MCP collects
|
|
231
|
+
* paths as text), the google-sign-in-running spinner (the MCP parks on
|
|
232
|
+
* 'google-sign-in' via its OAuth session), and view-workflow-diff (the MCP
|
|
233
|
+
* folds the diff into preview-workflow-file's context — 'view' re-parks);
|
|
234
|
+
* - the .p8 input chain, collapsed into the single 'ios-api-key' gate;
|
|
235
|
+
* - 'ask-build', mapped to the shared 'build-ready' choice (decideBuildPhase);
|
|
236
|
+
* - 'requesting-build', never run over MCP (the C2/D2 handoff + checkBuild
|
|
237
|
+
* polling replace it).
|
|
238
|
+
*/
|
|
239
|
+
export declare const MCP_UNREACHABLE_STEPS: ReadonlySet<string>;
|
|
123
240
|
/**
|
|
124
241
|
* Read-only "explain the current step" entry point backing the
|
|
125
242
|
* capgo_builder_onboarding_explain tool. Gathers facts (read-only) and returns a
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CapgoSDK } from '../../../sdk.js';
|
|
2
2
|
import type { EngineDeps } from './engine.js';
|
|
3
|
+
import type { BuildJobDeps } from './build-job.js';
|
|
4
|
+
import { type CredentialsManageDeps } from './credentials-manage.js';
|
|
3
5
|
/** Minimal shape of the MCP server's tool registrar (matches McpServer.tool). */
|
|
4
6
|
interface McpLike {
|
|
5
7
|
tool: (name: string, description: string, schema: Record<string, unknown>, handler: (args: any) => Promise<{
|
|
@@ -8,10 +10,24 @@ interface McpLike {
|
|
|
8
10
|
text: string;
|
|
9
11
|
}>;
|
|
10
12
|
}>) => unknown;
|
|
13
|
+
prompt?: (name: string, description: string, handler: () => {
|
|
14
|
+
messages: Array<{
|
|
15
|
+
role: 'user' | 'assistant';
|
|
16
|
+
content: {
|
|
17
|
+
type: 'text';
|
|
18
|
+
text: string;
|
|
19
|
+
};
|
|
20
|
+
}>;
|
|
21
|
+
}) => unknown;
|
|
11
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Production EngineDeps wiring. Exported for tests that pin production-only
|
|
25
|
+
* behavior (clearBuildRecord wiring, keystore file modes).
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildDeps(sdk: CapgoSDK): EngineDeps;
|
|
12
28
|
/**
|
|
13
29
|
* Register the 2-tool onboarding spine onto an MCP server.
|
|
14
30
|
* `depsOverride` is for tests; production passes only `server` + `sdk`.
|
|
15
31
|
*/
|
|
16
|
-
export declare function registerOnboardingTools(server: McpLike, sdk: CapgoSDK, depsOverride?: EngineDeps): void;
|
|
32
|
+
export declare function registerOnboardingTools(server: McpLike, sdk: CapgoSDK, depsOverride?: EngineDeps, buildJobDepsOverride?: BuildJobDeps, credentialsManageDepsOverride?: CredentialsManageDeps): void;
|
|
17
33
|
export {};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { CiSecretSetupAdvice, CiSecretTarget } from '../ci-secrets.js';
|
|
2
|
+
import type { IosEffectDeps } from '../ios/flow.js';
|
|
3
|
+
import type { TailEffectDeps } from '../tail/flow.js';
|
|
4
|
+
import type { OnboardingStep } from '../types.js';
|
|
5
|
+
import type { Platform } from './contract.js';
|
|
6
|
+
/**
|
|
7
|
+
* The iOS driver-held transient state — the exact `IosEffectDeps['carried']`
|
|
8
|
+
* shape, plus the MCP-only `parkedImportStep` (S12): the interactive import
|
|
9
|
+
* sub-flow prompt the driver parked between tool calls — the headless mirror
|
|
10
|
+
* of the TUI's React `step` state for the EPHEMERAL import prompts, which
|
|
11
|
+
* resume routing can never reproduce (see engine.ts iosParkedStep). NON-SECRET
|
|
12
|
+
* (a step name). Wiped with the session; a restart self-heals via a fresh
|
|
13
|
+
* import-scanning that re-derives the inventory and re-renders the picker.
|
|
14
|
+
*/
|
|
15
|
+
export type IosCarried = NonNullable<IosEffectDeps['carried']> & {
|
|
16
|
+
parkedImportStep?: OnboardingStep;
|
|
17
|
+
/**
|
|
18
|
+
* The chosen identity's Apple cert resource id (import-checking-apple-cert's
|
|
19
|
+
* transient — typed on IosStepCtx, not on the engine's carried shape). The
|
|
20
|
+
* registry really holds it after the transient merge; typing it here lets
|
|
21
|
+
* the driver DROP it when the user re-picks a different identity (the TUI
|
|
22
|
+
* clears its appleCertId mirror the same way). NON-SECRET (an Apple id).
|
|
23
|
+
*/
|
|
24
|
+
_appleCertIdForChosen?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The tail driver-held transient state — the exact `TailEffectDeps['carried']`
|
|
28
|
+
* shape, plus the NON-SECRET tail OUTCOME facts the outcome-aware terminal
|
|
29
|
+
* summary harvests (engine.ts harvestTailOutcomes → tailCompleteResult): the
|
|
30
|
+
* exact upload summary line (counts/labels only), the written workflow path
|
|
31
|
+
* and the exported .env path. These three are non-secret BY CONSTRUCTION and
|
|
32
|
+
* are the one carried subset allowed to surface verbatim in a tool result —
|
|
33
|
+
* secret VALUES (savedCredentials / ciSecretEntries) must still never leave
|
|
34
|
+
* this registry.
|
|
35
|
+
*/
|
|
36
|
+
export type TailCarried = NonNullable<TailEffectDeps['carried']> & {
|
|
37
|
+
ciSecretUploadSummary?: string;
|
|
38
|
+
workflowFilePath?: string;
|
|
39
|
+
envExportPath?: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* S9-S11: the MCP's parked interactive TAIL step + the NON-SECRET view context
|
|
43
|
+
* it was rendered with (option inventories / labels — never credential values).
|
|
44
|
+
* The TUI holds the current tail step in React state; the MCP mirrors it here so
|
|
45
|
+
* (a) the strict tail gate validates an answer against the step that actually
|
|
46
|
+
* asked, and (b) a re-render (corrective message, plain re-check) re-asks the
|
|
47
|
+
* SAME parked question instead of drifting forward through the resume router —
|
|
48
|
+
* which would collapse past consent gates like preview-workflow-file. A server
|
|
49
|
+
* restart loses the park; resume routing then takes over (the frozen
|
|
50
|
+
* tailResumeStep contract). EXPLICITLY NO SECRETS: ciSecretEntries (values)
|
|
51
|
+
* stay in tailCarried; only derived key NAMES may surface in tool results.
|
|
52
|
+
*/
|
|
53
|
+
export interface TailParkedState {
|
|
54
|
+
step: string;
|
|
55
|
+
ciSecretTargets?: CiSecretTarget[];
|
|
56
|
+
ciSecretSetupAdvice?: CiSecretSetupAdvice[];
|
|
57
|
+
ciSecretRepoLabel?: string | null;
|
|
58
|
+
ciSecretError?: string;
|
|
59
|
+
availableScripts?: Record<string, string>;
|
|
60
|
+
recommendedScript?: string | null;
|
|
61
|
+
}
|
|
62
|
+
export interface OnboardingSessionState {
|
|
63
|
+
iosCarried: IosCarried;
|
|
64
|
+
tailCarried: TailCarried;
|
|
65
|
+
tailParked?: TailParkedState;
|
|
66
|
+
/**
|
|
67
|
+
* The platform this session is setting up, as CHOSEN by the user (platform
|
|
68
|
+
* picker answer / single-platform auto-route / explicit `{ platform }`), NOT
|
|
69
|
+
* inferred from on-disk progress. This is what lets a bare `next_step({})`
|
|
70
|
+
* resume the right platform AND lets two concurrent server processes onboard
|
|
71
|
+
* the same app for different platforms without reading each other's progress
|
|
72
|
+
* files. Process-local: a restart loses it and the flow re-asks the picker.
|
|
73
|
+
*/
|
|
74
|
+
activePlatform?: Platform;
|
|
75
|
+
/**
|
|
76
|
+
* The platform whose continue-vs-restart resume prompt has already been
|
|
77
|
+
* resolved THIS session — shown and answered, or skipped as unnecessary (no
|
|
78
|
+
* resumable on-disk progress). While it differs from the committed platform, a
|
|
79
|
+
* fresh platform entry first shows the resume prompt (mirroring the TUI's
|
|
80
|
+
* `resume-prompt` fork) instead of silently resuming. Process-local: a restart
|
|
81
|
+
* loses it and the prompt is re-offered. Reset by runStart so a fresh "start
|
|
82
|
+
* onboarding" always re-asks.
|
|
83
|
+
*/
|
|
84
|
+
resumeResolvedFor?: Platform;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the session state for `appId`, creating an empty one on demand.
|
|
88
|
+
* The returned object is the current SNAPSHOT: merges replace the carried
|
|
89
|
+
* objects rather than mutating them, so re-read after merging.
|
|
90
|
+
*/
|
|
91
|
+
export declare function getSession(appId: string): OnboardingSessionState;
|
|
92
|
+
/**
|
|
93
|
+
* Read the platform this session committed to (via setSessionPlatform), or
|
|
94
|
+
* undefined when none has been chosen yet (fresh session, or after a restart).
|
|
95
|
+
* Deliberately does NOT consult disk progress — the platform is a session
|
|
96
|
+
* decision, not a property of what happens to be saved on disk.
|
|
97
|
+
*/
|
|
98
|
+
export declare function getSessionPlatform(appId: string): Platform | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Record (or, with `undefined`, clear) the platform this session is setting up.
|
|
101
|
+
* Called when the user picks at the platform gate, when a single-platform
|
|
102
|
+
* project auto-routes, or on any explicit `{ platform }`. Cleared by runStart so
|
|
103
|
+
* a fresh "start onboarding" always re-asks instead of silently resuming.
|
|
104
|
+
*/
|
|
105
|
+
export declare function setSessionPlatform(appId: string, platform: Platform | undefined): void;
|
|
106
|
+
/**
|
|
107
|
+
* Read which platform's resume prompt has already been resolved this session
|
|
108
|
+
* (shown+answered, or determined unnecessary), or undefined when none has.
|
|
109
|
+
* Process-local; deliberately ignores disk — the resume decision is a session
|
|
110
|
+
* decision, exactly like the platform itself.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getResumeResolvedFor(appId: string): Platform | undefined;
|
|
113
|
+
/**
|
|
114
|
+
* Record (or, with `undefined`, clear) which platform's continue-vs-restart
|
|
115
|
+
* resume prompt has been resolved this session. Set once the prompt is answered
|
|
116
|
+
* or skipped as unnecessary; cleared by runStart so a fresh start re-asks.
|
|
117
|
+
*/
|
|
118
|
+
export declare function setResumeResolvedFor(appId: string, platform: Platform | undefined): void;
|
|
119
|
+
/**
|
|
120
|
+
* Drop the per-flow carried transients (iOS carried, tail carried, tail park)
|
|
121
|
+
* for `appId` while PRESERVING the session-level decisions (activePlatform,
|
|
122
|
+
* resumeResolvedFor). Used by the resume prompt's "restart" arm: the on-disk
|
|
123
|
+
* progress is wiped, so the in-memory carried state from the old run must go
|
|
124
|
+
* too — but the session stays committed to the same platform and must not
|
|
125
|
+
* re-ask the resume prompt it just answered. Immutable: builds a NEW entry.
|
|
126
|
+
*/
|
|
127
|
+
export declare function clearSessionCarried(appId: string): void;
|
|
128
|
+
/**
|
|
129
|
+
* Merge `partial` into the iOS carried state for `appId` and return the new
|
|
130
|
+
* merged carried object. `undefined` values leave the prior value intact.
|
|
131
|
+
*/
|
|
132
|
+
export declare function mergeIosCarried(appId: string, partial: Partial<IosCarried>): IosCarried;
|
|
133
|
+
/**
|
|
134
|
+
* Merge `partial` into the tail carried state for `appId` and return the new
|
|
135
|
+
* merged carried object. `undefined` values leave the prior value intact.
|
|
136
|
+
*/
|
|
137
|
+
export declare function mergeTailCarried(appId: string, partial: Partial<TailCarried>): TailCarried;
|
|
138
|
+
/**
|
|
139
|
+
* Park the current interactive tail step (+ its non-secret view context) for
|
|
140
|
+
* `appId`. REPLACES any prior park — each render re-parks the step it shows,
|
|
141
|
+
* so the park always mirrors the question currently in front of the user.
|
|
142
|
+
* Immutable: builds a NEW session entry; prior snapshots are untouched.
|
|
143
|
+
*/
|
|
144
|
+
export declare function setTailParked(appId: string, parked: TailParkedState): void;
|
|
145
|
+
/**
|
|
146
|
+
* Drop `keys` from the iOS carried state for `appId` and return the new
|
|
147
|
+
* carried object. The complement of mergeIosCarried for one-shot consumable
|
|
148
|
+
* fields (e.g. the verify-app `verifyAction` pick, which the driver MUST clear
|
|
149
|
+
* after the resolver effect ran so a later re-entry runs the initial fetch —
|
|
150
|
+
* merge semantics skip `undefined`, so a merge can never clear). Immutable:
|
|
151
|
+
* builds a NEW carried object; prior snapshots are untouched.
|
|
152
|
+
*/
|
|
153
|
+
export declare function dropIosCarried(appId: string, keys: (keyof IosCarried)[]): IosCarried;
|
|
154
|
+
/**
|
|
155
|
+
* Drop the session entry for `appId`. Idempotent: safe on an absent appId and
|
|
156
|
+
* safe to call twice. The next getSession() recreates a fresh empty session.
|
|
157
|
+
*/
|
|
158
|
+
export declare function clearSession(appId: string): void;
|
|
159
|
+
/** Drop every session (test isolation only — production code clears per appId). */
|
|
160
|
+
export declare function clearAllSessions(): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AndroidOnboardingStep } from '../android/types.js';
|
|
2
|
+
import type { OnboardingStep } from '../types.js';
|
|
2
3
|
/** state → the set of input fields that legitimately answer it (in order). */
|
|
3
4
|
export declare const STEP_ALLOWED_FIELDS: Partial<Record<AndroidOnboardingStep, string[]>>;
|
|
4
5
|
/** The set of all android input keys we govern (for the extras check). */
|
|
@@ -10,8 +11,14 @@ export declare const ANDROID_INPUT_KEYS: string[];
|
|
|
10
11
|
* fields and no other governed android key. Otherwise { ok:false } with the
|
|
11
12
|
* allowed fields + the offending extra keys for a corrective message.
|
|
12
13
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Inputs with NO governed android key always pass (plain continue / other
|
|
15
|
+
* vocabularies). Steps with no allowed-field entry FAIL CLOSED for governed
|
|
16
|
+
* keys (hostile-review 2026-06-12): an android field sent at an auto step, at
|
|
17
|
+
* the google-sign-in park, or before the android flow has rendered its first
|
|
18
|
+
* step ('welcome', i.e. null progress) is never a legitimate answer — letting
|
|
19
|
+
* it through allowed jumping the keystore phase past the credentials-exist
|
|
20
|
+
* data-safety gate (which is seeded only when the flow renders
|
|
21
|
+
* keystore-method-select).
|
|
15
22
|
*
|
|
16
23
|
* @param currentStep the resume step the user is currently on
|
|
17
24
|
* @param input the next_step input object
|
|
@@ -43,3 +50,43 @@ export declare function validateStorePassword(currentStep: AndroidOnboardingStep
|
|
|
43
50
|
ok: boolean;
|
|
44
51
|
message?: string;
|
|
45
52
|
};
|
|
53
|
+
/** The set of all iOS input keys the MCP governs (for the extras check). */
|
|
54
|
+
export declare const IOS_INPUT_KEYS: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Validate incoming iOS next_step input against the step it answers.
|
|
57
|
+
*
|
|
58
|
+
* Returns { ok:true } when the input is a legitimate answer for `currentStep`
|
|
59
|
+
* (see the vocabulary rules above). Otherwise { ok:false, message } with a
|
|
60
|
+
* corrective instruction for the agent. Inputs with NO governed iOS key always
|
|
61
|
+
* pass — the android gate (and the rest of drive()) owns those.
|
|
62
|
+
*
|
|
63
|
+
* @param currentStep the iOS step the user is currently on (the session-parked
|
|
64
|
+
* recovery step when one is parked, else the resume step — see
|
|
65
|
+
* engine.ts effectiveIosStep)
|
|
66
|
+
* @param input the next_step input object
|
|
67
|
+
*/
|
|
68
|
+
export declare function validateIosStepInput(currentStep: OnboardingStep, input: Record<string, unknown>): {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
message?: string;
|
|
71
|
+
};
|
|
72
|
+
/** The tail family answer fields (one per step family; envExportPath is the ask-export-env companion). */
|
|
73
|
+
export declare const TAIL_FAMILY_FIELDS: readonly ["ciSecretAction", "githubActionsSetup", "exportEnvAction", "packageManager", "buildScript", "buildScriptCustom", "workflowFileAction"];
|
|
74
|
+
/** Every tail input key the MCP governs (for presence/extras checks). */
|
|
75
|
+
export declare const TAIL_INPUT_KEYS: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Validate an incoming tail next_step input against the step it answers.
|
|
78
|
+
*
|
|
79
|
+
* @param currentStep the EFFECTIVE tail step (the session-parked step when one
|
|
80
|
+
* is parked, else the platform resume step — see engine.ts)
|
|
81
|
+
* @param input the next_step input object
|
|
82
|
+
* @param ctx optional parked inventories for the dynamic vocabularies
|
|
83
|
+
*/
|
|
84
|
+
export declare function validateTailStepInput(currentStep: string, input: Record<string, unknown>, ctx?: {
|
|
85
|
+
ciSecretTargets?: {
|
|
86
|
+
provider: string;
|
|
87
|
+
}[];
|
|
88
|
+
availableScripts?: Record<string, string>;
|
|
89
|
+
}): {
|
|
90
|
+
ok: boolean;
|
|
91
|
+
message?: string;
|
|
92
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AndroidOnboardingProgress } from '../android/types.js';
|
|
2
|
+
import type { OnboardingProgress } from '../types.js';
|
|
3
|
+
/** Build the slim iOS tail progress — WHITELIST ONLY (see module doc). */
|
|
4
|
+
export declare function slimIosTailProgress(progress: OnboardingProgress): OnboardingProgress;
|
|
5
|
+
/** Build the slim Android tail progress — WHITELIST ONLY (see module doc). */
|
|
6
|
+
export declare function slimAndroidTailProgress(progress: AndroidOnboardingProgress): AndroidOnboardingProgress;
|
|
@@ -18,7 +18,7 @@ export interface OnboardingResult {
|
|
|
18
18
|
summary?: OnboardingCompletionSummary;
|
|
19
19
|
}
|
|
20
20
|
export type OnboardingStep = 'welcome' | 'resume-prompt' | 'platform-select' | 'adding-platform' | 'credentials-exist' | 'backing-up' | 'setup-method-select' | 'import-scanning' | 'import-distribution-mode' | 'import-pick-identity' | 'import-pick-profile' | 'import-validating-all-certs' | 'import-checking-apple-cert' | 'import-no-match-recovery' | 'import-portal-explanation' | 'import-provide-profile-path' | 'import-create-profile-only' | 'import-export-warning' | 'import-exporting' | 'p8-source-select' | 'asc-key-generating' | 'asc-key-created' | 'api-key-instructions' | 'p8-method-select' | 'input-p8-path' | 'input-key-id' | 'input-issuer-id' | 'verifying-key' | 'verify-app' | 'creating-certificate' | 'cert-limit-prompt' | 'revoking-certificate' | 'creating-profile' | 'duplicate-profile-prompt' | 'deleting-duplicate-profiles' | 'saving-credentials' | 'detecting-ci-secrets' | 'ci-secrets-setup' | 'ci-secrets-target-select' | 'ask-ci-secrets' | 'checking-ci-secrets' | 'confirm-ci-secret-overwrite' | 'uploading-ci-secrets' | 'ci-secrets-failed' | 'ask-github-actions-setup' | 'confirm-secrets-push' | 'ask-export-env' | 'exporting-env' | 'confirm-env-export-overwrite' | 'overwrite-and-export-env' | 'pick-package-manager' | 'pick-build-script' | 'pick-build-script-custom' | 'preview-workflow-file' | 'view-workflow-diff' | 'writing-workflow-file' | 'ask-build' | 'requesting-build' | 'ai-analysis-prompt' | 'ai-analysis-running' | 'ai-analysis-result' | 'ai-analysis-result-scroll' | 'build-complete' | 'no-platform' | 'error' | 'support-confirm' | 'support-log-view' | 'support-uploading';
|
|
21
|
-
export type OnboardingErrorCategory = 'apple_api_unauthorized' | 'apple_api_rate_limited' | 'cert_limit_reached' | 'profile_creation_failed' | 'p8_invalid' | 'keychain_no_identities' | 'keychain_export_failed' | 'profile_no_match' | 'profile_read_failed' | 'unknown';
|
|
21
|
+
export type OnboardingErrorCategory = 'apple_api_unauthorized' | 'apple_api_forbidden' | 'apple_agreements_missing' | 'apple_api_rate_limited' | 'cert_limit_reached' | 'profile_creation_failed' | 'p8_invalid' | 'keychain_no_identities' | 'keychain_export_failed' | 'profile_no_match' | 'profile_read_failed' | 'unknown';
|
|
22
22
|
export interface ApiKeyData {
|
|
23
23
|
keyId: string;
|
|
24
24
|
issuerId: string;
|
|
@@ -26,6 +26,13 @@ export interface WriteBuildOutputRecordInput {
|
|
|
26
26
|
*
|
|
27
27
|
* Failures rendering the PNG are non-fatal — the JSON is always written. The
|
|
28
28
|
* record's `qrCodePngPath` field is null when the PNG could not be produced.
|
|
29
|
+
*
|
|
30
|
+
* Hardening (hostile-review 2026-06-12): the record and PNG paths are unlinked
|
|
31
|
+
* before writing so a pre-planted symlink is replaced instead of followed; the
|
|
32
|
+
* record is written with mode 0600 (outputUrl is a signed download URL); a
|
|
33
|
+
* created parent directory gets mode 0700; and when the record lives under the
|
|
34
|
+
* shared tmpdir, a parent directory that is a symlink or owned by another user
|
|
35
|
+
* is refused.
|
|
29
36
|
*/
|
|
30
37
|
export declare function writeBuildOutputRecord(recordPath: string, input: WriteBuildOutputRecordInput, onWarn?: (msg: string) => void): Promise<BuildOutputRecord>;
|
|
31
38
|
/**
|
|
@@ -36,12 +43,44 @@ export declare function writeBuildOutputRecord(recordPath: string, input: WriteB
|
|
|
36
43
|
*
|
|
37
44
|
* appId is sanitized: all `/` and `\` characters are replaced with `_`, and
|
|
38
45
|
* any `..` sequences are replaced with `_`, to prevent path traversal.
|
|
46
|
+
*
|
|
47
|
+
* The record lives in a per-user `capgo-build-records-<uid>` subdirectory
|
|
48
|
+
* (created with mode 0700 by `writeBuildOutputRecord`) so that on shared-tmp
|
|
49
|
+
* systems another user can neither pre-create nor read the record file
|
|
50
|
+
* (hostile-review 2026-06-12).
|
|
39
51
|
*/
|
|
40
52
|
export declare function defaultBuildRecordPath(appId: string, platform: 'ios' | 'android'): string;
|
|
41
53
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
54
|
+
* Remove a build output record and its companion QR PNG. Absent paths are a
|
|
55
|
+
* no-op. Called before a new build hand-off so a record left behind by an
|
|
56
|
+
* earlier build can never be read as the new build's result.
|
|
57
|
+
*/
|
|
58
|
+
export declare function removeBuildOutputRecord(recordPath: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Thrown by `readBuildOutputRecord` when the record file EXISTS but cannot be
|
|
61
|
+
* used: unreadable (permissions), a symbolic link, not valid JSON (e.g. a
|
|
62
|
+
* truncated write), or an unexpected shape. Distinct from the `null` return
|
|
63
|
+
* (no record yet) so callers polling for a build result can surface the
|
|
64
|
+
* failure instead of waiting forever.
|
|
65
|
+
*/
|
|
66
|
+
export declare class BuildRecordReadError extends Error {
|
|
67
|
+
readonly recordPath: string;
|
|
68
|
+
constructor(message: string, recordPath: string);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read a build output record from `path`.
|
|
72
|
+
*
|
|
73
|
+
* Returns `null` ONLY when the file does not exist yet (ENOENT — the build has
|
|
74
|
+
* not finished). Every other failure mode (unreadable file, a symlink at the
|
|
75
|
+
* record path, malformed JSON, missing/wrong-type fields) throws
|
|
76
|
+
* `BuildRecordReadError`: a present-but-corrupt record is a surfaced failure,
|
|
77
|
+
* never "still waiting".
|
|
78
|
+
*
|
|
79
|
+
* Shape rules (hostile-review 2026-06-12):
|
|
80
|
+
* - every record owes the `jobId`/`status`/`outputUrl` trio (forward-tolerant
|
|
81
|
+
* baseline for future schemaVersions);
|
|
82
|
+
* - a `schemaVersion: 1` record is validated strictly against the full
|
|
83
|
+
* `BuildOutputRecord` shape — the v1 writer has always emitted it, and
|
|
84
|
+
* checkBuild correlates `appId`/`platform` against the build it launched.
|
|
46
85
|
*/
|
|
47
86
|
export declare function readBuildOutputRecord(path: string): Promise<BuildOutputRecord | null>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server `instructions` — the connect-time guidance string handed to clients
|
|
3
|
+
* (Codex, Claude Code, …) in the `initialize` result. Clients that support it inject
|
|
4
|
+
* this text into the model's context, so it is the one cross-client, server-side lever
|
|
5
|
+
* for steering WHEN to reach for these tools (tool descriptions only steer WHICH tool
|
|
6
|
+
* once the model has already decided to use the server).
|
|
7
|
+
*
|
|
8
|
+
* The general Capgo Cloud capabilities are always described. The Builder-onboarding
|
|
9
|
+
* steer is appended only when the onboarding tools are actually registered, so we never
|
|
10
|
+
* advertise a `start_capgo_builder_onboarding` tool that isn't there.
|
|
11
|
+
*
|
|
12
|
+
* Keep the result under 512 characters: some clients (Codex) cap server instructions
|
|
13
|
+
* at that length. `test/test-mcp-instructions.mjs` pins this.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildServerInstructions(onboardingEnabled: boolean): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
/**
|
|
3
|
+
* Harden a stdio MCP server against stdout pollution.
|
|
4
|
+
*
|
|
5
|
+
* The MCP stdio transport frames JSON-RPC over `process.stdout`. Any *other* write to
|
|
6
|
+
* stdout — a clack `intro`/`log`, a stray `console.log`, a chatty dependency — injects
|
|
7
|
+
* non-JSON bytes into that stream, and a strict client (e.g. Codex) then drops the
|
|
8
|
+
* connection with "Transport closed". Per-call `silent` flags (see sdk.ts) are easy to
|
|
9
|
+
* miss one-by-one, so this is the backstop: it routes EVERY ambient `process.stdout`
|
|
10
|
+
* write to stderr (still visible for debugging, harmless to the protocol) and returns a
|
|
11
|
+
* dedicated Writable — bound to the real stdout — for the transport to send JSON-RPC on.
|
|
12
|
+
*
|
|
13
|
+
* Call once, before constructing StdioServerTransport, and hand it the returned stream:
|
|
14
|
+
* const out = installMcpStdoutGuard()
|
|
15
|
+
* const transport = new StdioServerTransport(process.stdin, out)
|
|
16
|
+
*
|
|
17
|
+
* @returns a Writable wired to the real fd-1 stdout, for the transport only.
|
|
18
|
+
*/
|
|
19
|
+
export declare function installMcpStdoutGuard(): Writable;
|