halfcycle 0.4.14 → 0.4.15
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/.claude-plugin/plugin.json +1 -1
- package/bin/bin.bundle.mjs +2525 -541
- package/bin/pre-tool-use-filter.sh +41 -5
- package/commands/agent-teams-orchestration.md +1 -1
- package/commands/consistency-check.md +1 -1
- package/commands/context-update.md +1 -1
- package/commands/halfcycle-design.md +1 -1
- package/commands/halfcycle-setup.md +1 -1
- package/commands/stack-assembly-worker.md +1 -1
- package/commands/task-execution.md +1 -1
- package/commands/task-graph.md +1 -1
- package/commands/write-spec.md +1 -1
- package/dist/bin.js +0 -0
- package/dist/build-record/render-v2.d.ts.map +1 -1
- package/dist/claude-plan.d.ts +24 -5
- package/dist/claude-plan.d.ts.map +1 -1
- package/dist/cli-contract.d.ts +1 -1
- package/dist/cli-contract.d.ts.map +1 -1
- package/dist/cli.js +2242 -565
- package/dist/cli.js.map +3 -3
- package/dist/close-phase.d.ts.map +1 -1
- package/dist/doctor.d.ts +34 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/folder-refusals.d.ts +27 -0
- package/dist/folder-refusals.d.ts.map +1 -0
- package/dist/identity.d.ts +31 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/index.js +1276 -386
- package/dist/index.js.map +3 -3
- package/dist/install-plan.d.ts +112 -0
- package/dist/install-plan.d.ts.map +1 -0
- package/dist/install.d.ts +535 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/plan.d.ts +16 -3
- package/dist/plan.d.ts.map +1 -1
- package/dist/pre-create-checks.d.ts +28 -0
- package/dist/pre-create-checks.d.ts.map +1 -1
- package/dist/preview.d.ts +40 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/restore.d.ts +58 -0
- package/dist/restore.d.ts.map +1 -0
- package/dist/review.d.ts.map +1 -1
- package/dist/scan.d.ts +11 -0
- package/dist/scan.d.ts.map +1 -1
- package/dist/setup/discovery.d.ts.map +1 -1
- package/dist/setup/rows.d.ts +10 -6
- package/dist/setup/rows.d.ts.map +1 -1
- package/package.json +12 -12
package/dist/install.d.ts
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* Halfcycle's scaffold contents.
|
|
23
23
|
*/
|
|
24
24
|
import { BUNDLE_PIN_REL, MCP_REGISTRATION_REL, MCP_SERVER_KEY, SETTINGS_REL, WRITE_CAPABLE_TOOLS } from '@halfcycle/core';
|
|
25
|
-
import {
|
|
25
|
+
import { type PlannedProjectIdentity } from './identity.js';
|
|
26
|
+
import { lineKey, type InstallManifest, type ManifestFile } from './install-manifest.js';
|
|
26
27
|
import { type ClaudeSettings, type StatusLineSetting } from './merge-settings.js';
|
|
27
28
|
export type EngagementType = 'self-build' | 'client';
|
|
28
29
|
export interface BundlePin {
|
|
@@ -376,6 +377,8 @@ export { BUNDLE_PIN_REL };
|
|
|
376
377
|
/** The crew roster. Exported for the monorepo uninstall (`self-build-install` AC8),
|
|
377
378
|
* which must name this one path to check its bytes rather than remove it by path. */
|
|
378
379
|
export declare const CREW_ROSTER_REL = ".halfcycle/crew.json";
|
|
380
|
+
/** The captured-fixture index, written only when absent. */
|
|
381
|
+
export declare const CAPTURED_INDEX_REL = "test/fixtures/captured/manifest.json";
|
|
379
382
|
/**
|
|
380
383
|
* The three files the installer merges into rather than owns. `SETTINGS_REL` lives
|
|
381
384
|
* with the hook table in `@halfcycle/core` and is re-exported here, as is
|
|
@@ -385,6 +388,15 @@ export { SETTINGS_REL, WRITE_CAPABLE_TOOLS };
|
|
|
385
388
|
export declare const GITIGNORE_REL = ".gitignore";
|
|
386
389
|
/** The attributes file the installer appends its line-ending rules to. */
|
|
387
390
|
export declare const GITATTRIBUTES_REL = ".gitattributes";
|
|
391
|
+
/**
|
|
392
|
+
* The connection-headers helper an earlier version generated and `.mcp.json` named
|
|
393
|
+
* (T-24, #446). RETIRED by `windows-client-seat`: Claude Code runs a `headersHelper`
|
|
394
|
+
* under `cmd.exe` on Windows, where this POSIX script cannot run, so the registration
|
|
395
|
+
* now names a `node` command (`MCP_HEADERS_HELPER_COMMAND`) and the script's work is
|
|
396
|
+
* the vendored runner's `mcp-headers` verb. Declared here, above the list it joins,
|
|
397
|
+
* because that list is read at module load.
|
|
398
|
+
*/
|
|
399
|
+
export declare const RETIRED_MCP_HEADERS_HELPER_REL = ".halfcycle/mcp-headers.sh";
|
|
388
400
|
export interface ClosedWriteSet {
|
|
389
401
|
/** Files the installer writes whole. */
|
|
390
402
|
readonly files: ReadonlySet<string>;
|
|
@@ -487,6 +499,65 @@ export declare function closedWriteSet(): ClosedWriteSet;
|
|
|
487
499
|
* (`startManifest`), so a committed record cannot vouch for a path that is not ours.
|
|
488
500
|
*/
|
|
489
501
|
export declare function bytesAreOurs(path: string, bytes: Uint8Array, recorded: ManifestFile | undefined, writeSet: ClosedWriteSet): boolean;
|
|
502
|
+
/**
|
|
503
|
+
* Read the manifest an earlier install wrote, for this run to carry forward.
|
|
504
|
+
*
|
|
505
|
+
* AN UNREADABLE ONE IS TREATED AS ABSENT, and that is the safe direction here (it is
|
|
506
|
+
* the opposite of uninstall, which refuses one). This run is about to rewrite it
|
|
507
|
+
* from what it observes; the cost of reading it as absent is that this run is
|
|
508
|
+
* treated as the first manifest run, which claims LESS — nothing it cannot prove —
|
|
509
|
+
* and marks the merged files adopted rather than created, which makes uninstall
|
|
510
|
+
* remove less. Refusing the install over a file the install is about to rewrite
|
|
511
|
+
* would leave a tree with a corrupt manifest no command could repair.
|
|
512
|
+
*/
|
|
513
|
+
declare function readPreviousManifest(targetRepo: string): InstallManifest | null;
|
|
514
|
+
/** The bytes a path holds once the install has run (see `noteFile`). */
|
|
515
|
+
type BytesAfter = (rel: string) => Uint8Array;
|
|
516
|
+
/**
|
|
517
|
+
* Which files set up here git would leave out of a commit (`setup-travel-warning`), or
|
|
518
|
+
* `null` when there is nothing to say.
|
|
519
|
+
*
|
|
520
|
+
* THE PATHS COME FROM THE MANIFEST THIS RUN WROTE, NEVER FROM THE HOOK TABLE (decision
|
|
521
|
+
* 15). The table is what a machine runs; this is every file the installer set up, and
|
|
522
|
+
* a new installer path joins it by being recorded, which `assertManifestInWriteSet`
|
|
523
|
+
* already holds to the closed write set. So: the manifest's files except the
|
|
524
|
+
* per-machine one, the merged files it records, and the manifest itself — less this
|
|
525
|
+
* run's collisions (a developer's own file at one of our names was not set up here,
|
|
526
|
+
* even when an earlier manifest recorded the name) and less anything absent from disk.
|
|
527
|
+
*
|
|
528
|
+
* A path is dropped too when a directory above it, below `targetRepo`, is a symbolic
|
|
529
|
+
* link or holds a `.git` entry (a submodule or nested repository). Git refuses the
|
|
530
|
+
* whole batch over one such path, so a symlinked or submodule `.claude` would
|
|
531
|
+
* otherwise silence an ignored `.halfcycle/` or `.mcp.json`. A symlinked FILE is
|
|
532
|
+
* answered normally and stays.
|
|
533
|
+
*
|
|
534
|
+
* The named file is `.claude/settings.json` when it is ignored, else the first ignored
|
|
535
|
+
* path in sorted order. Reads only: nothing here writes to the tree or to git.
|
|
536
|
+
*/
|
|
537
|
+
declare function findNotShared(targetRepo: string, manifest: InstallManifest, collidedPaths: readonly string[]): NotShared | null;
|
|
538
|
+
/** A planned command stub: a `PlannedFile` plus whether a collision is an edit of ours (E8). */
|
|
539
|
+
export interface PlannedCommand extends PlannedFile {
|
|
540
|
+
readonly editedCollided: boolean;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* One file the installer's plan decided about (`install-plan.ts`), at a path inside the
|
|
544
|
+
* folder. `text` is what the file holds once the plan is applied: the bytes written for
|
|
545
|
+
* `written`, the bytes already there for `skipped`, and for `collided` the bytes that
|
|
546
|
+
* would have been written, which are not. `replacedExisting` is true only when a
|
|
547
|
+
* DIFFERING file of ours is overwritten, never on a fresh write or an identical one.
|
|
548
|
+
*/
|
|
549
|
+
export interface PlannedFile {
|
|
550
|
+
readonly rel: string;
|
|
551
|
+
readonly text: string;
|
|
552
|
+
readonly outcome: CopyOutcome;
|
|
553
|
+
readonly replacedExisting: boolean;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* The ONE write a planned file gets (`setup-preview-and-doctor` AC1): its planned bytes,
|
|
557
|
+
* at its path, only when the plan says `written`, and only inside the write allowlist.
|
|
558
|
+
* The decision was taken by the plan; this never re-reads the file to take it again.
|
|
559
|
+
*/
|
|
560
|
+
export declare function putPlannedFile(targetRepoRoot: string, planned: PlannedFile): void;
|
|
490
561
|
/**
|
|
491
562
|
* The write outcome for one target path under the copy set.
|
|
492
563
|
* - `written` — the file did not exist and was created.
|
|
@@ -503,6 +574,19 @@ type CopyOutcome = 'written' | 'skipped' | 'collided';
|
|
|
503
574
|
* whose enforcement nothing exercises is the shape this area keeps paying for.
|
|
504
575
|
* It is not an API: every caller inside this module goes through `recordOwned`. */
|
|
505
576
|
export declare function writeOwned(targetAbsPath: string, targetRepoRoot: string, content: string): CopyOutcome;
|
|
577
|
+
/** The four path buckets the install accumulates and returns. */
|
|
578
|
+
interface MutableReport {
|
|
579
|
+
writtenPaths: string[];
|
|
580
|
+
skippedPaths: string[];
|
|
581
|
+
mergedPaths: string[];
|
|
582
|
+
collidedPaths: string[];
|
|
583
|
+
/** A SUBSET of `collidedPaths` (E8): the previous manifest already named this
|
|
584
|
+
* path as ours. See `InstallResult.editedCollidedPaths`. */
|
|
585
|
+
editedCollidedPaths: string[];
|
|
586
|
+
/** A SUBSET of `writtenPaths`, not a fifth bucket: files of ours that already
|
|
587
|
+
* existed, differed, and were overwritten (`GWF-2`). See `recordOwnedGenerated`. */
|
|
588
|
+
replacedPaths: string[];
|
|
589
|
+
}
|
|
506
590
|
/**
|
|
507
591
|
* The `permissions.deny` patterns a fresh install seeds (F-23(a), operator
|
|
508
592
|
* decision note 2026-08-10). THE ONE HOME of the list — the method's served step
|
|
@@ -713,6 +797,12 @@ export type RetiredFileRemoval = 'removed' | 'kept-foreign' | 'kept-uncommitted'
|
|
|
713
797
|
* otherwise-successful install.
|
|
714
798
|
*/
|
|
715
799
|
export declare function removeLegacyCiStanza(targetRepo: string): RetiredFileRemoval;
|
|
800
|
+
/** The removal half: only ever reached for a file the plan decided `removed`. Best-effort. */
|
|
801
|
+
export declare function removePlannedRetiredFile(targetRepo: string, rel: string): 'removed' | 'failed';
|
|
802
|
+
/** The path of the CI stanza an earlier version wrote; nothing writes it any more. */
|
|
803
|
+
export declare const LEGACY_CI_STANZA_REL = ".halfcycle/ci-stanza.yml";
|
|
804
|
+
/** `removeLegacyCiStanza`'s decision, by reading alone. */
|
|
805
|
+
export declare function planLegacyCiStanzaRemoval(targetRepo: string): RetiredFileRemoval;
|
|
716
806
|
/**
|
|
717
807
|
* **THE INSTALLER WRITES NO `.env.example`, AND THIS IS WHERE THAT GENERATOR WAS.**
|
|
718
808
|
*
|
|
@@ -776,6 +866,8 @@ export { MCP_REGISTRATION_REL, MCP_SERVER_KEY };
|
|
|
776
866
|
* with no MCP origin rewrites no registration, so it removes nothing.
|
|
777
867
|
*/
|
|
778
868
|
export declare function removeRetiredMcpHeadersHelper(targetRepo: string): RetiredFileRemoval;
|
|
869
|
+
/** `removeRetiredMcpHeadersHelper`'s decision, by reading alone. */
|
|
870
|
+
export declare function planMcpHeadersHelperRemoval(targetRepo: string): RetiredFileRemoval;
|
|
779
871
|
/**
|
|
780
872
|
* THE ONE HOME of *which generated files this installer claims as its own, and by
|
|
781
873
|
* what evidence* (#1103). Every entry is a client-tree path this package
|
|
@@ -818,6 +910,7 @@ export declare const OWNED_GENERATED_HEADERS: {
|
|
|
818
910
|
readonly '.claude/hooks/guard-runner.sh': "# Governance hook wrapper — generated by";
|
|
819
911
|
readonly '.claude/hooks/user-prompt-reminder.sh': "# UserPromptSubmit house-rule reminder — generated by";
|
|
820
912
|
readonly '.claude/hooks/user-prompt-scope.sh': "# Typed scope approval — generated by";
|
|
913
|
+
readonly '.claude/hooks/user-prompt-findings.sh': "# Findings hand-over — generated by";
|
|
821
914
|
readonly '.claude/hooks/session-end-marker.sh': "# Session-end marker cleanup — generated by";
|
|
822
915
|
readonly '.claude/hooks/session-start-marker.sh': "# Session-start marker — generated by";
|
|
823
916
|
};
|
|
@@ -870,6 +963,32 @@ export declare function lineEndingOf(text: string): '\n' | '\r\n';
|
|
|
870
963
|
* CRLF script, which is why `.gitattributes` pins them LF in every clone.
|
|
871
964
|
*/
|
|
872
965
|
export declare function inLineEndingOf(existing: string | null, generated: string): string;
|
|
966
|
+
/**
|
|
967
|
+
* Ensure `.gitignore` carries every required entry, reconciled BY CONTENT — a
|
|
968
|
+
* newer required entry is appended even on a tree an older install wrote.
|
|
969
|
+
*
|
|
970
|
+
* BEST-EFFORT BY DESIGN: a write failure (e.g. a read-only `.gitignore`) does NOT
|
|
971
|
+
* throw here — and since T-09 there is no longer a hard gate downstream of it,
|
|
972
|
+
* because the thing that gate protected (a bearer token in the tree) is not written
|
|
973
|
+
* any more. What is left on this path is per-device state and Zone-B, whose worst
|
|
974
|
+
* case is a noisy `git status`, not a leaked credential. `failed` is reported so the operator
|
|
975
|
+
* sees the reconcile did not land.
|
|
976
|
+
*
|
|
977
|
+
* @returns the outcome for the report bucket.
|
|
978
|
+
*/
|
|
979
|
+
export declare function planGitignore(targetRepo: string): PlannedMerge;
|
|
980
|
+
/** The write half of the `.gitignore` reconcile: the planned bytes, or `failed`. Never throws. */
|
|
981
|
+
export declare function putPlannedGitignore(targetRepo: string, planned: PlannedMerge): CopyOutcome | 'failed';
|
|
982
|
+
/**
|
|
983
|
+
* A planned reconcile of a file the installer appends to (`.gitignore`, `.gitattributes`):
|
|
984
|
+
* the text before, the text after (`null` when there is none to read), and the outcome.
|
|
985
|
+
* `failed` means the plan already knows the write cannot land; nothing is attempted.
|
|
986
|
+
*/
|
|
987
|
+
export interface PlannedMerge {
|
|
988
|
+
readonly before: string | null;
|
|
989
|
+
readonly after: string | null;
|
|
990
|
+
readonly outcome: CopyOutcome | 'failed';
|
|
991
|
+
}
|
|
873
992
|
/**
|
|
874
993
|
* The header line every `.gitattributes` block this installer appends opens with
|
|
875
994
|
* (`windows-install-hygiene.md`, AC1). Git for Windows usually clones with
|
|
@@ -877,6 +996,99 @@ export declare function inLineEndingOf(existing: string | null, generated: strin
|
|
|
877
996
|
* line endings; the rules under this header pin them to LF in every clone.
|
|
878
997
|
*/
|
|
879
998
|
export declare const GITATTRIBUTES_HEADER = "# Halfcycle \u2014 generated scripts keep LF line endings";
|
|
999
|
+
/** A file's text, or `null` when it is absent or cannot be read as a file. */
|
|
1000
|
+
export declare function readTextOrNull(path: string): string | null;
|
|
1001
|
+
/**
|
|
1002
|
+
* Ensure `.gitattributes` carries every required LF rule, reconciled BY CONTENT the
|
|
1003
|
+
* way `reconcileGitignore` reconciles `.gitignore`: a missing rule is appended under
|
|
1004
|
+
* the header, a rule already present (the developer's or an earlier run's) is never
|
|
1005
|
+
* appended twice, and no existing byte is rewritten.
|
|
1006
|
+
*
|
|
1007
|
+
* Routed through `writeAllowlisted`, so this write is held to `WRITE_ALLOWLIST` —
|
|
1008
|
+
* which `reconcileGitignore`'s bare write is not. BEST-EFFORT, as that one is: a
|
|
1009
|
+
* failure (a read-only file, or the allowlist refusing) is caught and returned as
|
|
1010
|
+
* `failed`, and the install goes on. The worst case of a missing rule is a hook
|
|
1011
|
+
* script a Windows clone checks out with CR line endings.
|
|
1012
|
+
*
|
|
1013
|
+
* A SYMBOLIC LINK at `.gitattributes` is refused as `failed`, never written through:
|
|
1014
|
+
* the allowlist checks the path's name, and a link could point outside the tree.
|
|
1015
|
+
*/
|
|
1016
|
+
export declare function planGitattributes(targetRepo: string): PlannedMerge;
|
|
1017
|
+
/** The write half of the `.gitattributes` reconcile, held to the allowlist. Never throws. */
|
|
1018
|
+
export declare function putPlannedGitattributes(targetRepo: string, planned: PlannedMerge): CopyOutcome | 'failed';
|
|
1019
|
+
/**
|
|
1020
|
+
* Decide the committed pin's bytes (`writeBundlePin` writes them). Reads only.
|
|
1021
|
+
*
|
|
1022
|
+
* THE CREATOR'S ID IS CARRIED FORWARD, and that is the whole reason this function reads
|
|
1023
|
+
* before it writes. The pin is rewritten from scratch on every install, and only the
|
|
1024
|
+
* create arm supplies the id (E1, #1598); a re-run, a join and a refused join supply
|
|
1025
|
+
* none. Composing a fresh pin from one of those would silently DELETE the record the
|
|
1026
|
+
* create wrote — the exact disappearance this field exists to prevent — and letting
|
|
1027
|
+
* one of them supply its own acting account (the pre-E1 rule) made a joiner in a clone
|
|
1028
|
+
* rewrite the committed owner to itself. So the previous value wins over any supplied
|
|
1029
|
+
* one, and a pin carrying the pre-E1 name `accountId` is read through
|
|
1030
|
+
* `pinCreatedByAccountId` and written back as `createdByAccountId`.
|
|
1031
|
+
*
|
|
1032
|
+
* **`installedAt` IS CARRIED FORWARD TOO, ALWAYS, WITHIN THE SAME ENGAGEMENT (F-02,
|
|
1033
|
+
* #1192 — H-04 walk observation D, 2026-09-17).** The field means THE MOMENT THIS
|
|
1034
|
+
* ENGAGEMENT WAS FIRST SET UP IN THIS REPOSITORY — not the moment the last install ran,
|
|
1035
|
+
* and not anything about a machine. Until this fix it was `new Date()` on every write, so
|
|
1036
|
+
* `npx halfcycle` in a second clone of an already-set-up project (the join arm: same
|
|
1037
|
+
* engagement, same version, same account) rewrote the committed pin with a new timestamp
|
|
1038
|
+
* and left that clone's tree dirty with a change nobody meant to commit — and the
|
|
1039
|
+
* criterion-8 test pinned that as a documented exemption rather than as the defect it
|
|
1040
|
+
* was. A committed file must not carry a per-run value: what it records is a fact about
|
|
1041
|
+
* the repository, which every clone shares, so a re-run that changes no fact must leave
|
|
1042
|
+
* the bytes alone. The rule is therefore the same one the creator's id already follows —
|
|
1043
|
+
* carried within the same engagement, fresh for a different one — and a re-install of the
|
|
1044
|
+
* same engagement at the same version by any account is byte-identical. A version
|
|
1045
|
+
* upgrade changes `version` and nothing else: the set-up moment did not move because the
|
|
1046
|
+
* installer did.
|
|
1047
|
+
*
|
|
1048
|
+
* IT IS CARRIED ONLY WITHIN THE SAME ENGAGEMENT. A run that installs a different
|
|
1049
|
+
* engagement id into this repository (an explicit id argument, or a join) is a different
|
|
1050
|
+
* tenancy, and the previous engagement's creator is not a fact about it: such a run is
|
|
1051
|
+
* not a create, so the pin records nothing rather than a name inherited from an
|
|
1052
|
+
* engagement it has nothing to do with. The same
|
|
1053
|
+
* holds for the set-up moment: a repository re-pointed at another engagement was set up
|
|
1054
|
+
* for THAT engagement now.
|
|
1055
|
+
*
|
|
1056
|
+
* A pin that cannot be read is treated as no pin. This is advisory provenance, and an
|
|
1057
|
+
* unparseable file must not fail an install. A previous pin whose `installedAt` is
|
|
1058
|
+
* missing or blank gets this run's clock — the only value there is — rather than an
|
|
1059
|
+
* empty field.
|
|
1060
|
+
*/
|
|
1061
|
+
declare function planBundlePin(targetRepoRoot: string, version: string, engagementId: string, engagementType: EngagementType, createdByAccountId: string | undefined, methodVersion: string | undefined): PlannedFile;
|
|
1062
|
+
/**
|
|
1063
|
+
* The preview's best prediction for the committed pin, read from this repository's OWN
|
|
1064
|
+
* pin and credential store rather than from the service (W-10 R2, #F-65; tightened by
|
|
1065
|
+
* review R-F65 finding 1). A preview never signs in, so it has no FRESH answer for the
|
|
1066
|
+
* method version — and a real run only ever carries the committed pin's method version
|
|
1067
|
+
* forward UNCHANGED, with no network call of its own, on exactly one arm: the RE-USE
|
|
1068
|
+
* arm (`bin.ts`), reached when this machine already holds a complete, usable credential
|
|
1069
|
+
* for this SAME engagement (`readPinnedEngagement`), because `toCredential` never
|
|
1070
|
+
* stores a method version in that local store at all, so `planBundlePin` always falls
|
|
1071
|
+
* back to the previous pin's own value there (`carriedMethodVersion`, above). Every
|
|
1072
|
+
* OTHER real run — a join from a second clone, a fresh container, a credential this
|
|
1073
|
+
* machine never had — asks a live plane for a method version this offline preview
|
|
1074
|
+
* cannot know, and the plane can answer something the committed pin does not expect (a
|
|
1075
|
+
* project whose phase moved on since this pin was last written). Claiming `leave` there
|
|
1076
|
+
* is a guess, not a prediction, and review R-F65 found it wrong in exactly that shape:
|
|
1077
|
+
* a repository pinned to an older method, joined while the service now serves a newer
|
|
1078
|
+
* one, previewed as unchanged and then was not. So this function predicts `leave` (or
|
|
1079
|
+
* an exact `change`) ONLY on the re-use arm; every other case returns `null`, and
|
|
1080
|
+
* `install-plan.ts` leaves the row `PENDING` — `change` by existence alone — exactly as
|
|
1081
|
+
* AC1/AC3 already document for a file whose bytes need the service's fresh answer.
|
|
1082
|
+
* `home` is the preview's own (a test's throwaway, or the real one), read-only, same as
|
|
1083
|
+
* everything else `planInstall` already reads from a folder it never signs in from.
|
|
1084
|
+
*/
|
|
1085
|
+
export declare function planBundlePinPreview(targetRepoRoot: string, version: string, home?: string): PlannedFile | null;
|
|
1086
|
+
/**
|
|
1087
|
+
* Write the committed pin a plan decided (`planBundlePin`), through `writeAllowlisted`
|
|
1088
|
+
* alone, so identical bytes are no write and a changed pin lands in `writtenPaths`. One of
|
|
1089
|
+
* the two functions allowed to write the pin.
|
|
1090
|
+
*/
|
|
1091
|
+
declare function writeBundlePin(targetRepoRoot: string, planned: PlannedFile, writtenPaths: string[]): void;
|
|
880
1092
|
/**
|
|
881
1093
|
* Rewrite the committed pin's `methodVersion` field after `close-phase` or
|
|
882
1094
|
* `open-phase` moves the project forward between phases
|
|
@@ -1070,6 +1282,179 @@ export declare function checkDrift(targetRepoRoot: string): {
|
|
|
1070
1282
|
/** The method version the committed pin recorded (T-01), or `null` when none was. */
|
|
1071
1283
|
methodVersion: string | null;
|
|
1072
1284
|
};
|
|
1285
|
+
/**
|
|
1286
|
+
* Create `.halfcycle/project.json` (committed identity) when there is none. The file
|
|
1287
|
+
* carries no secret — only the minted id and the repo's root-commit/remote
|
|
1288
|
+
* fingerprint (criterion 2 / operator decision (au)).
|
|
1289
|
+
*
|
|
1290
|
+
* ONE RULE: AN EXISTING `project.json` IS NEVER WRITTEN (`project-identity-reuse`
|
|
1291
|
+
* AC1). Until the onboarding fix wave (T-05, from review R-03) this reconciled by
|
|
1292
|
+
* bytes and rewrote any file that differed from what this installer would write —
|
|
1293
|
+
* an older installer's file without `note`, CRLF line endings, an extra field — by
|
|
1294
|
+
* truncate-then-write, which also raced a concurrent install's read of the same
|
|
1295
|
+
* file. Now a present file is `skipped` whatever its bytes, and an absent one is
|
|
1296
|
+
* created through `createProjectIdentityFile`, whose `linkSync` never replaces a
|
|
1297
|
+
* file that appeared in between. The cost, accepted: an older file never gains the
|
|
1298
|
+
* current `note` (W8-T-04, F-4); a fresh install still writes it.
|
|
1299
|
+
*
|
|
1300
|
+
* **ONE EXCEPTION (#1744, G1): `repairRemoteCredential` runs on a present file
|
|
1301
|
+
* before it is reported `skipped`**, and rewrites it ONLY when its `remote` still
|
|
1302
|
+
* carries a credential — review R-34, F1: this is THE real install path every
|
|
1303
|
+
* run takes (`bin.ts` calls `install()` on every arm), and the first cut of this
|
|
1304
|
+
* fix lived only in `mintOrReadIdentity`, which nothing here ever called, so a
|
|
1305
|
+
* real run left the credential in place. Best-effort and silent either way.
|
|
1306
|
+
*
|
|
1307
|
+
* **WRITTEN FROM THE PLAN (`setup-preview-and-doctor` AC1).** What happens here was
|
|
1308
|
+
* decided by `planProjectIdentity` before anything was written: `add` creates the file
|
|
1309
|
+
* with the identity the plan minted, `change` is the G1 repair with the plan's bytes,
|
|
1310
|
+
* `leave` writes nothing. The create arm's `claimFolderId` calls this same function
|
|
1311
|
+
* with `probe` set, so its write-access check runs on every call, as it always has.
|
|
1312
|
+
*
|
|
1313
|
+
* @returns the outcome for the report bucket: `written` for `add`, else `skipped`.
|
|
1314
|
+
*/
|
|
1315
|
+
export declare function writeProjectIdentity(targetRepo: string, planned: PlannedProjectIdentity, probe?: boolean): CopyOutcome;
|
|
1316
|
+
/**
|
|
1317
|
+
* Persist the engagement's credential to the MACHINE-LEVEL store, outside this
|
|
1318
|
+
* repository (T-09) — `~/.halfcycle/engagements/<id>/env`, owner-only.
|
|
1319
|
+
*
|
|
1320
|
+
* **The refusal contract that used to live here is RETIRED, not forgotten.**
|
|
1321
|
+
* Operator decision (av) made this function throw when `.gitignore` did not cover
|
|
1322
|
+
* `.env.local`, so a live bearer token was never written into a tree that would
|
|
1323
|
+
* commit it on the next `git add .`. Nothing is written into the tree any more, so
|
|
1324
|
+
* that refusal now protects a file that does not exist — and a builder who kept it
|
|
1325
|
+
* would refuse installs over its absence. The property it defended is stronger
|
|
1326
|
+
* without it: the credential cannot be committed because it is not there, rather
|
|
1327
|
+
* than because a tracked ignore file says not to.
|
|
1328
|
+
*
|
|
1329
|
+
* NON-DESTRUCTIVE, still: the store is APPEND-RECONCILED by key
|
|
1330
|
+
* (`reconcileEnvText`), because Studio's provider writes its own keys into the same
|
|
1331
|
+
* per-engagement file — `CONTROL_TELEMETRY_TOKEN` and `HALFCYCLE_GUARD_EVAL_LOG_DIR`
|
|
1332
|
+
* — and a whole-file overwrite here would silently delete them.
|
|
1333
|
+
*
|
|
1334
|
+
* **`CONTROL_TELEMETRY_URL` is NOT in that "Studio's own key" set, and saying it was
|
|
1335
|
+
* would be a false claim (review finding, T-10 follow-up).** This docblock named
|
|
1336
|
+
* `CONTROL_TELEMETRY_*` as a whole family until this correction, which was true
|
|
1337
|
+
* before T-10 and stopped being true once `CONTROL_TELEMETRY_URL` joined
|
|
1338
|
+
* `ENGAGEMENT_ENV_KEYS` — it is reconciled BY THIS installer now, exactly like
|
|
1339
|
+
* `HALFCYCLE_TOKEN`, and a Studio-written value for it is overwritten on the next
|
|
1340
|
+
* Connect install rather than preserved. `CONTROL_TELEMETRY_TOKEN` alone is the
|
|
1341
|
+
* still-genuinely-foreign example.
|
|
1342
|
+
*
|
|
1343
|
+
* The engagement id travels into the store as a value, and no name reaches the
|
|
1344
|
+
* client's tree at all: since 2026-08-30 the installer generates no env scaffold, so
|
|
1345
|
+
* what this engagement was created AGAINST is recorded only here, at machine level.
|
|
1346
|
+
*/
|
|
1347
|
+
declare function writeEngagementCredential(credential: DeviceCredential, engagementId: string, home?: string): CopyOutcome;
|
|
1348
|
+
/**
|
|
1349
|
+
* What the installer's plan expects the machine-level store to hold once the credential
|
|
1350
|
+
* is written, and whether that write changes it — computed with the store's own
|
|
1351
|
+
* reconcile (`reconcileEnvText`), reading the store and writing nothing. The legacy
|
|
1352
|
+
* migration is PLANNED against `storedAfter`; it is still DECIDED again at apply time
|
|
1353
|
+
* against the store read back from disk (`migrateLegacyEnvLocal`'s rule).
|
|
1354
|
+
*/
|
|
1355
|
+
export declare function planEngagementCredential(credential: DeviceCredential, engagementId: string, home?: string): {
|
|
1356
|
+
outcome: CopyOutcome;
|
|
1357
|
+
existed: boolean;
|
|
1358
|
+
storedAfter: Record<string, string>;
|
|
1359
|
+
};
|
|
1360
|
+
/** Where the credential used to live, before T-09. Named once, and only the
|
|
1361
|
+
* migration below may use it. */
|
|
1362
|
+
export declare const LEGACY_ENV_LOCAL_REL = ".env.local";
|
|
1363
|
+
/** What the legacy-credential migration did, for the installer's report. */
|
|
1364
|
+
export interface LegacyMigration {
|
|
1365
|
+
outcome: 'removed' | 'stripped' | 'failed' | 'absent' | 'kept';
|
|
1366
|
+
/**
|
|
1367
|
+
* On `kept` ONLY: the Halfcycle keys the in-tree file carries that the
|
|
1368
|
+
* machine-level store does not, i.e. the values that would have been destroyed.
|
|
1369
|
+
* Empty on every other outcome.
|
|
1370
|
+
*/
|
|
1371
|
+
unbacked: string[];
|
|
1372
|
+
/**
|
|
1373
|
+
* On `kept` ONLY: which of `CREDENTIAL_REQUIRED_KEYS` this file cannot supply —
|
|
1374
|
+
* the actual REASON the store was not written, derived rather than narrated. A
|
|
1375
|
+
* pre-#439 repo is missing `HALFCYCLE_MCP_URL` and may or may not be missing
|
|
1376
|
+
* `GUARD_SERVICE_URL`; a message that guesses which would be wrong for half of them.
|
|
1377
|
+
*/
|
|
1378
|
+
missingForAdoption: string[];
|
|
1379
|
+
/** On a `stripped` DECISION only: the file's text with our keys taken out. */
|
|
1380
|
+
keptText?: string;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* DECIDE (and since the plan-then-apply split, only decide; `applyLegacyMigration`
|
|
1384
|
+
* writes) taking the seven Halfcycle keys OUT of a pre-T-09 `.env.local` — but ONLY once the
|
|
1385
|
+
* machine-level store demonstrably holds them — and delete the file if that is all
|
|
1386
|
+
* it held.
|
|
1387
|
+
*
|
|
1388
|
+
* **This is the step that makes the feature true for a repository that already
|
|
1389
|
+
* exists.** Writing the machine-level store while leaving the old file in place
|
|
1390
|
+
* would satisfy every "a fresh install writes no credential" assertion and leave a
|
|
1391
|
+
* live bearer token in the tree of every repo installed before today — the exact
|
|
1392
|
+
* exposure this task closes, surviving in the population that actually has one.
|
|
1393
|
+
*
|
|
1394
|
+
* ---------------------------------------------------------------------------
|
|
1395
|
+
* **THE RULE IS: NEVER REMOVE THIS REPOSITORY'S COPY UNLESS A COPY NOW EXISTS
|
|
1396
|
+
* OUTSIDE IT.** `stored` is the machine-level store READ BACK FROM DISK after the
|
|
1397
|
+
* write at step 7c, and this function strips nothing that is not in it.
|
|
1398
|
+
*
|
|
1399
|
+
* This is not defensive dressing; it is the review blocker. The first version ran
|
|
1400
|
+
* unconditionally, on the stated ground that a repo reaching this point must have
|
|
1401
|
+
* supplied a credential. It need not, and **the population where it does not is
|
|
1402
|
+
* exactly the population the migration exists for.** `toCredential` yields a
|
|
1403
|
+
* credential only when ALL FOUR of `HALFCYCLE_SERVICE_URL`, `HALFCYCLE_TOKEN`,
|
|
1404
|
+
* `HALFCYCLE_MCP_URL` and `GUARD_SERVICE_URL` are present; `HALFCYCLE_MCP_URL`
|
|
1405
|
+
* entered the write set on 2026-08-08 (#439) and `GUARD_SERVICE_URL` on 2026-08-10
|
|
1406
|
+
* (FW-4). Every engagement installed before those dates has a `.env.local` holding
|
|
1407
|
+
* a live token and NOT those keys — so the store write was skipped, and this
|
|
1408
|
+
* function deleted the only copy of a session token that cannot be re-minted, then
|
|
1409
|
+
* reported *"REMOVED — the credential now lives outside this repository"*, at exit
|
|
1410
|
+
* 0, when nothing had been written outside the repository at all.
|
|
1411
|
+
*
|
|
1412
|
+
* Keying on the store's CONTENTS rather than on "was a credential passed in" is
|
|
1413
|
+
* deliberate: a partial or truncated write reads as missing keys here and takes the
|
|
1414
|
+
* same refusal, whereas the `credential !== undefined` test would call it a success.
|
|
1415
|
+
* The comparison is PRESENCE, not value equality — a store holding a NEWER token
|
|
1416
|
+
* for the same engagement must still be allowed to retire the stale in-tree copy,
|
|
1417
|
+
* which is the whole point; what may never happen is dropping a key the store has
|
|
1418
|
+
* no value for at all.
|
|
1419
|
+
*
|
|
1420
|
+
* And the copy that remains must be USABLE, not merely present — see the two
|
|
1421
|
+
* conditions at the refusal below. A store that holds some of the keys but cannot
|
|
1422
|
+
* produce a whole `DeviceCredential` leaves the NEXT command with nothing in either
|
|
1423
|
+
* place, which is a quieter failure than this one.
|
|
1424
|
+
*
|
|
1425
|
+
* ---------------------------------------------------------------------------
|
|
1426
|
+
* **IT REMOVES OUR KEYS, NOT THE DEVELOPER'S FILE.** `.env.local` is where a
|
|
1427
|
+
* developer keeps their own `DATABASE_URL` — that is why the write path was made
|
|
1428
|
+
* append-reconciling in the first place (#375) — so deleting it wholesale would
|
|
1429
|
+
* destroy work the installer never owned. Every line that is not one of ours
|
|
1430
|
+
* survives byte-for-byte, and the file is `unlink`ed only when what remains is
|
|
1431
|
+
* nothing but our own header and blank lines.
|
|
1432
|
+
*
|
|
1433
|
+
* **A file holding NONE of our keys is not ours to touch, whatever else is in it.**
|
|
1434
|
+
* The removal test used to be "what survives the strip is only blank lines and our
|
|
1435
|
+
* header", which is true of a file the installer never wrote — an empty
|
|
1436
|
+
* `.env.local`, or one holding a single `# Halfcycle …` note of the developer's own
|
|
1437
|
+
* — so those were deleted and reported as the credential move. The presence of one
|
|
1438
|
+
* of our keys is now the gate on touching the file at all.
|
|
1439
|
+
*
|
|
1440
|
+
* BEST-EFFORT on the rewrite: a failure (a read-only file, say) is reported, never
|
|
1441
|
+
* thrown. The credential is already safe in the machine store by the time a rewrite
|
|
1442
|
+
* is attempted; failing the whole install over a file we are trying to clean up
|
|
1443
|
+
* would turn a successful migration into a broken one.
|
|
1444
|
+
*/
|
|
1445
|
+
export declare function migrateLegacyEnvLocal(targetRepo: string, stored: Record<string, string>): LegacyMigration;
|
|
1446
|
+
/**
|
|
1447
|
+
* The write half of the migration: carry out a `removed` or `stripped` decision,
|
|
1448
|
+
* best-effort (`failed`, never thrown). Every other decision writes nothing.
|
|
1449
|
+
*
|
|
1450
|
+
* **THE APPLY DECIDES AGAIN, ON PURPOSE.** The plan decides this migration against the
|
|
1451
|
+
* store it EXPECTS after the credential write (`planEngagementCredential`), which is
|
|
1452
|
+
* what the preview lists. The rule above is that the tree's copy goes only once a copy
|
|
1453
|
+
* demonstrably lives outside it, so `applyInstallPlan` reads the store back from disk
|
|
1454
|
+
* after its credential write and calls `migrateLegacyEnvLocal` again; only that
|
|
1455
|
+
* decision reaches this function. A plan's prediction never deletes a credential.
|
|
1456
|
+
*/
|
|
1457
|
+
export declare function applyLegacyMigration(targetRepo: string, decided: LegacyMigration): LegacyMigration;
|
|
1073
1458
|
/**
|
|
1074
1459
|
* Install the bundle into a target engagement repo.
|
|
1075
1460
|
*
|
|
@@ -1096,6 +1481,155 @@ export declare function checkDrift(targetRepoRoot: string): {
|
|
|
1096
1481
|
* 11. Whether git ignores any of it — read, never changed (`setup-travel-warning`)
|
|
1097
1482
|
*/
|
|
1098
1483
|
export declare function install(options: InstallOptions): Promise<InstallResult>;
|
|
1484
|
+
/**
|
|
1485
|
+
* Every decision the installer's plan takes, in write order. Built by `planInstall`
|
|
1486
|
+
* (`install-plan.ts`) from the planning functions below, applied by
|
|
1487
|
+
* `applyInstallPlan`, and turned into the report and the install record by
|
|
1488
|
+
* `accountInstall` — the same function for the plan's prediction and the apply's
|
|
1489
|
+
* result, so the two cannot list different paths.
|
|
1490
|
+
*
|
|
1491
|
+
* `engagementId` absent is a PREVIEW: the service's answer (the project id, the
|
|
1492
|
+
* credential, the method version) is still to come, so the pin, the server entry and
|
|
1493
|
+
* the install record are `pending`, and such steps cannot be applied.
|
|
1494
|
+
*/
|
|
1495
|
+
export interface InstallSteps {
|
|
1496
|
+
readonly targetRepo: string;
|
|
1497
|
+
readonly version: string;
|
|
1498
|
+
readonly engagementId: string | undefined;
|
|
1499
|
+
readonly engagementType: EngagementType | undefined;
|
|
1500
|
+
readonly credential: DeviceCredential | undefined;
|
|
1501
|
+
readonly home: string | undefined;
|
|
1502
|
+
readonly writeSet: ClosedWriteSet;
|
|
1503
|
+
readonly previous: InstallManifest | null;
|
|
1504
|
+
/** The write set's directories already there BEFORE anything is written. */
|
|
1505
|
+
readonly dirsBefore: ReadonlySet<string>;
|
|
1506
|
+
readonly commands: readonly PlannedCommand[];
|
|
1507
|
+
readonly vendored: PlannedFile;
|
|
1508
|
+
readonly settings: PlannedSettings;
|
|
1509
|
+
readonly hooks: readonly PlannedFile[];
|
|
1510
|
+
readonly ciStanza: RetiredFileRemoval;
|
|
1511
|
+
readonly mcp: PlannedMcp;
|
|
1512
|
+
/** The retired connection-headers helper; `null` when no server entry is written. */
|
|
1513
|
+
readonly mcpHelper: RetiredFileRemoval | null;
|
|
1514
|
+
readonly gitignore: PlannedMerge;
|
|
1515
|
+
readonly gitattributes: PlannedMerge;
|
|
1516
|
+
readonly identity: PlannedProjectIdentity;
|
|
1517
|
+
/** `null` when no credential is supplied (nothing is written outside the folder). */
|
|
1518
|
+
readonly credentialWrite: {
|
|
1519
|
+
readonly outcome: CopyOutcome;
|
|
1520
|
+
readonly existed: boolean;
|
|
1521
|
+
readonly path: string;
|
|
1522
|
+
} | null;
|
|
1523
|
+
/** The legacy migration as PLANNED; the apply decides it again (see `applyLegacyMigration`). */
|
|
1524
|
+
readonly legacy: LegacyMigration;
|
|
1525
|
+
/** `null` while the project id is pending (a preview). */
|
|
1526
|
+
readonly pin: PlannedFile | null;
|
|
1527
|
+
readonly crew: PlannedFile;
|
|
1528
|
+
readonly scan: {
|
|
1529
|
+
readonly text: string | null;
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
/** The `.claude/settings.json` merge as planned. */
|
|
1533
|
+
export interface PlannedSettings {
|
|
1534
|
+
readonly file: PlannedFile;
|
|
1535
|
+
readonly preexisted: boolean;
|
|
1536
|
+
readonly existing: ClaudeSettings;
|
|
1537
|
+
readonly generated: ClaudeSettings;
|
|
1538
|
+
readonly merged: ClaudeSettings;
|
|
1539
|
+
readonly retiredOurs: readonly string[];
|
|
1540
|
+
}
|
|
1541
|
+
/** The `.mcp.json` server entry as planned. */
|
|
1542
|
+
export type PlannedMcp = {
|
|
1543
|
+
readonly kind: 'write';
|
|
1544
|
+
readonly existing: string | null;
|
|
1545
|
+
readonly text: string;
|
|
1546
|
+
readonly outcome: 'written' | 'merged' | 'skipped';
|
|
1547
|
+
} | {
|
|
1548
|
+
readonly kind: 'none';
|
|
1549
|
+
readonly existing: string | null;
|
|
1550
|
+
} | {
|
|
1551
|
+
readonly kind: 'pending';
|
|
1552
|
+
readonly existing: string | null;
|
|
1553
|
+
};
|
|
1554
|
+
/** The plugin's version and the command stubs it declares, decided against this folder. */
|
|
1555
|
+
export declare function planCommandStubs(targetRepo: string, previous: InstallManifest | null, writeSet: ClosedWriteSet): {
|
|
1556
|
+
version: string;
|
|
1557
|
+
commands: PlannedCommand[];
|
|
1558
|
+
};
|
|
1559
|
+
/** The install record an earlier install left here, or `null`. Reads only. */
|
|
1560
|
+
export { readPreviousManifest };
|
|
1561
|
+
/**
|
|
1562
|
+
* 4. The vendored guard binary — the SELF-CONTAINED esbuild output at
|
|
1563
|
+
* `.halfcycle/bin/bin.bundle.mjs`, owned BY PATH (T-08 walk, F-2): collision-safety froze
|
|
1564
|
+
* it at whichever build first landed. It is ~600 kB with no header this package
|
|
1565
|
+
* controls, so evidence-based ownership is not available here — a constraint, not a choice.
|
|
1566
|
+
*/
|
|
1567
|
+
export declare function planVendoredBinary(targetRepo: string): PlannedFile;
|
|
1568
|
+
/**
|
|
1569
|
+
* 5. `.claude/settings.json` — MERGED, never replaced (criterion 4): every developer key
|
|
1570
|
+
* kept, Halfcycle's hook entries upserted by identity. An unparseable file throws
|
|
1571
|
+
* `SettingsParseRefused` with the same sentence the create arm prints (T-16, #1567).
|
|
1572
|
+
* The retired deny rules this install's own record says Halfcycle added are taken out
|
|
1573
|
+
* once; the status line is offered only when none is set elsewhere (`builds-paced-to-plan`
|
|
1574
|
+
* AC7), those other files read and never written.
|
|
1575
|
+
*/
|
|
1576
|
+
export declare function planSettings(targetRepo: string, previous: InstallManifest | null, home: string | undefined, env: NodeJS.ProcessEnv | undefined): PlannedSettings;
|
|
1577
|
+
/**
|
|
1578
|
+
* EVERY GENERATED HOOK SCRIPT, decided through `decideOwnedGenerated` — the criterion-5
|
|
1579
|
+
* rule (#1103; G-02, #1065): a file carrying OUR generated header prefix on its header
|
|
1580
|
+
* line is replaced, one that does not is `collided`. The freeze collision-safety caused
|
|
1581
|
+
* was walked, not inferred (#1103).
|
|
1582
|
+
*/
|
|
1583
|
+
export declare function planHookScripts(targetRepo: string): PlannedFile[];
|
|
1584
|
+
/**
|
|
1585
|
+
* 6b. The remote MCP registration (`.mcp.json`, T-11) — merged with the developer's own
|
|
1586
|
+
* servers, the credential carried BY REFERENCE only (`MCP_HEADERS_HELPER_COMMAND`).
|
|
1587
|
+
* Written ONLY when the MCP origin is known (T-24): no credential is `none`, and a
|
|
1588
|
+
* preview, whose credential is still to come, is `pending`.
|
|
1589
|
+
*/
|
|
1590
|
+
export declare function planMcpRegistration(targetRepo: string, credential: DeviceCredential | 'pending' | undefined): PlannedMcp;
|
|
1591
|
+
/** The write half of the server entry, through `writeAllowlisted`. */
|
|
1592
|
+
export declare function putPlannedMcp(targetRepo: string, planned: PlannedMcp): void;
|
|
1593
|
+
/** 8b. The crew roster, owned by path (see `writeCrewRoster`). */
|
|
1594
|
+
export declare function planCrewRoster(targetRepo: string): PlannedFile;
|
|
1595
|
+
/** 10. The install record, owned by path, decided once its bytes are known. */
|
|
1596
|
+
export declare function planInstallRecord(targetRepo: string, text: string): PlannedFile;
|
|
1597
|
+
export { writeBundlePin, planBundlePin, writeEngagementCredential, findNotShared };
|
|
1598
|
+
/**
|
|
1599
|
+
* What actually happened at the apply, for the steps whose write can fail without
|
|
1600
|
+
* throwing (or, for the legacy migration, is decided again against the store read back
|
|
1601
|
+
* from disk). For the plan's own prediction these are the planned outcomes.
|
|
1602
|
+
*/
|
|
1603
|
+
export interface InstallOutcomes {
|
|
1604
|
+
readonly ciStanza: RetiredFileRemoval;
|
|
1605
|
+
readonly mcpHelper: RetiredFileRemoval | null;
|
|
1606
|
+
readonly gitignore: CopyOutcome | 'failed';
|
|
1607
|
+
readonly gitignoreAfter: string | null;
|
|
1608
|
+
readonly gitattributes: CopyOutcome | 'failed';
|
|
1609
|
+
readonly gitattributesAfter: string | null;
|
|
1610
|
+
readonly credential: CopyOutcome | null;
|
|
1611
|
+
readonly legacy: LegacyMigration;
|
|
1612
|
+
/** The bytes a path holds once the install has run (read back by the apply). */
|
|
1613
|
+
readonly bytesAfter: BytesAfter;
|
|
1614
|
+
/** Whether a write-set directory exists once the install has run. */
|
|
1615
|
+
readonly dirExistsAfter: (dir: string) => boolean;
|
|
1616
|
+
/** Whether `.halfcycle/state.json` exists once the install has run. */
|
|
1617
|
+
readonly stateExistsAfter: boolean;
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Build the install's report and its install record from the plan's steps and what
|
|
1621
|
+
* happened (`InstallOutcomes`): the ONE place either is assembled, so the plan's
|
|
1622
|
+
* prediction and the apply's result list the same paths by construction. Writes
|
|
1623
|
+
* nothing. Requires a complete plan (no `pending` step).
|
|
1624
|
+
*/
|
|
1625
|
+
export declare function accountInstall(steps: InstallSteps, actual: InstallOutcomes): {
|
|
1626
|
+
report: MutableReport;
|
|
1627
|
+
installManifest: InstallManifest;
|
|
1628
|
+
manifestText: string;
|
|
1629
|
+
};
|
|
1630
|
+
/** Record the install record's own write, last in the report (step 10). */
|
|
1631
|
+
export declare function recordInstallRecord(report: MutableReport, planned: PlannedFile): void;
|
|
1632
|
+
export type { MutableReport, CopyOutcome, BytesAfter };
|
|
1099
1633
|
/** The one ignore line `.halfcycle/.gitignore` holds: the runner copy stays out of git.
|
|
1100
1634
|
* Exported: the monorepo uninstall (`self-build-install` AC8) removes this path by
|
|
1101
1635
|
* name from the same constant. */
|