@p4code/cli 0.2.4 → 0.2.5
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/bin.mjs +672 -46
- package/dist/client/assets/{DiffPanel-CkrZfPGI.js → DiffPanel-Do6aSu0E.js} +3 -3
- package/dist/client/assets/FilePreviewPanel-D2U3PXVf.js +2230 -0
- package/dist/client/assets/PreviewPanel-BuuYvjac.js +2 -0
- package/dist/client/assets/{PullRequestCodeTab-DXvCq9PN.js → PullRequestCodeTab-vbrYmAeQ.js} +4 -4
- package/dist/client/assets/arrow-right-B2X51S6S.js +2 -0
- package/dist/client/assets/{fileCommentAnnotations-jIuAO40K.js → fileCommentAnnotations-BZ_k09XR.js} +2 -2
- package/dist/client/assets/{index-BZdqP0xD.js → index-IyXnER5Q.js} +1535 -241
- package/dist/client/assets/pierre-dark-CpLgRqie.js +2 -0
- package/dist/client/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +2 -0
- package/dist/client/assets/pierre-dark-soft-kZQmAZld.js +2 -0
- package/dist/client/assets/pierre-dark-tritanopia-CpjhbsIL.js +2 -0
- package/dist/client/assets/pierre-dark-vibrant-CpQYzh95.js +2 -0
- package/dist/client/assets/pierre-light-CoaEpmwp.js +2 -0
- package/dist/client/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +2 -0
- package/dist/client/assets/pierre-light-soft-lWLdNTOI.js +2 -0
- package/dist/client/assets/pierre-light-tritanopia-CEbqgOJL.js +2 -0
- package/dist/client/assets/pierre-light-vibrant-D80Fkn33.js +2 -0
- package/dist/client/assets/renderFileChildren-uw3E6Jd2.js +2 -0
- package/dist/client/assets/terminal-links-C6S74E9U.js +47 -0
- package/dist/client/assets/toggle-group-CxBIRzrW.js +2 -0
- package/dist/client/assets/{worker-aIZfs-9q.js → worker-CTbBhd8-.js} +24 -21
- package/dist/client/index.html +2 -2
- package/package.json +2 -2
- package/dist/client/assets/FilePreviewPanel-ExZ7jSHB.js +0 -1778
- package/dist/client/assets/PreviewPanel-DFC5gUqR.js +0 -2
- package/dist/client/assets/arrow-right-BsoyxJft.js +0 -2
- package/dist/client/assets/pierre-dark-sU4Zdns8.js +0 -2
- package/dist/client/assets/pierre-dark-soft-HXgun5vs.js +0 -2
- package/dist/client/assets/pierre-dark-vibrant-D4RhcSIK.js +0 -2
- package/dist/client/assets/pierre-light-DWBk51d8.js +0 -2
- package/dist/client/assets/pierre-light-soft-CodEzPxf.js +0 -2
- package/dist/client/assets/pierre-light-vibrant-B76X7i5Y.js +0 -2
- package/dist/client/assets/renderFileChildren-CbnyGaFt.js +0 -2
- package/dist/client/assets/terminal-links-CaZNZumK.js +0 -47
- package/dist/client/assets/toggle-group-Aw7xeuHS.js +0 -2
package/dist/bin.mjs
CHANGED
|
@@ -237,7 +237,7 @@ const make$87 = () => {
|
|
|
237
237
|
const layer$79 = Layer.sync(NetService, make$87);
|
|
238
238
|
//#endregion
|
|
239
239
|
//#region package.json
|
|
240
|
-
var version = "0.2.
|
|
240
|
+
var version = "0.2.5";
|
|
241
241
|
//#endregion
|
|
242
242
|
//#region src/config.ts
|
|
243
243
|
/**
|
|
@@ -964,6 +964,41 @@ const PortSchema = Schema$1.Int.check(Schema$1.isBetween({
|
|
|
964
964
|
}));
|
|
965
965
|
const IsoDateTime = Schema$1.String;
|
|
966
966
|
/**
|
|
967
|
+
* Wire codec for server→client arrays whose element unions grow over time (new
|
|
968
|
+
* literal members, new struct variants). Decoding drops elements this build
|
|
969
|
+
* cannot decode instead of failing the whole payload — a client has to keep
|
|
970
|
+
* decoding configs sent by servers newer than itself, and rejecting the payload
|
|
971
|
+
* would take the connection down over data the client could not act on anyway.
|
|
972
|
+
* Encoding is the plain array encoding.
|
|
973
|
+
*/
|
|
974
|
+
const ForwardCompatibleArray = (element) => {
|
|
975
|
+
const decodeElement = Schema$1.decodeUnknownOption(element);
|
|
976
|
+
return Schema$1.Array(Schema$1.Unknown).pipe(Schema$1.decodeTo(Schema$1.Array(element), SchemaTransformation.transform({
|
|
977
|
+
decode: (values) => values.filter((value) => Option.isSome(decodeElement(value))),
|
|
978
|
+
encode: (values) => values
|
|
979
|
+
})));
|
|
980
|
+
};
|
|
981
|
+
/**
|
|
982
|
+
* Wire codec for a scalar literal union that grows over time.
|
|
983
|
+
*
|
|
984
|
+
* The sibling of {@link ForwardCompatibleArray}, for the case where the union
|
|
985
|
+
* is a field inside a struct rather than an array element: dropping the value
|
|
986
|
+
* is not an option, so an unrecognised literal decodes to `fallback` instead of
|
|
987
|
+
* failing the struct - and with it every struct that contains it.
|
|
988
|
+
*
|
|
989
|
+
* The fallback has to be a member this build already renders as "nothing
|
|
990
|
+
* actionable here", never one that asserts something specific about a state
|
|
991
|
+
* this build cannot know.
|
|
992
|
+
*/
|
|
993
|
+
const LiteralWithFallback = (members, fallback) => {
|
|
994
|
+
const literals = Schema$1.Literals(members);
|
|
995
|
+
const decodeLiteral = Schema$1.decodeUnknownOption(literals);
|
|
996
|
+
return Schema$1.String.pipe(Schema$1.decodeTo(literals, SchemaTransformation.transform({
|
|
997
|
+
decode: (value) => Option.isSome(decodeLiteral(value)) ? value : fallback,
|
|
998
|
+
encode: (value) => value
|
|
999
|
+
})));
|
|
1000
|
+
};
|
|
1001
|
+
/**
|
|
967
1002
|
* Construct a branded identifier. Enforces non-empty trimmed strings
|
|
968
1003
|
*/
|
|
969
1004
|
const makeEntityId = (brand) => {
|
|
@@ -8018,19 +8053,18 @@ const KeybindingsInvalidEntryIssue = Schema$1.Struct({
|
|
|
8018
8053
|
message: TrimmedNonEmptyString,
|
|
8019
8054
|
index: Schema$1.Number
|
|
8020
8055
|
});
|
|
8021
|
-
const
|
|
8022
|
-
const
|
|
8023
|
-
const ServerProviderState = Schema$1.Literals([
|
|
8056
|
+
const ServerConfigIssues = ForwardCompatibleArray(Schema$1.Union([KeybindingsMalformedConfigIssue, KeybindingsInvalidEntryIssue]));
|
|
8057
|
+
const ServerProviderState = LiteralWithFallback([
|
|
8024
8058
|
"ready",
|
|
8025
8059
|
"warning",
|
|
8026
8060
|
"error",
|
|
8027
8061
|
"disabled"
|
|
8028
|
-
]);
|
|
8029
|
-
const ServerProviderAuthStatus =
|
|
8062
|
+
], "warning");
|
|
8063
|
+
const ServerProviderAuthStatus = LiteralWithFallback([
|
|
8030
8064
|
"authenticated",
|
|
8031
8065
|
"unauthenticated",
|
|
8032
8066
|
"unknown"
|
|
8033
|
-
]);
|
|
8067
|
+
], "unknown");
|
|
8034
8068
|
const ServerProviderAuth = Schema$1.Struct({
|
|
8035
8069
|
status: ServerProviderAuthStatus,
|
|
8036
8070
|
type: Schema$1.optional(TrimmedNonEmptyString),
|
|
@@ -8080,11 +8114,11 @@ const ServerProviderSkill = Schema$1.Struct({
|
|
|
8080
8114
|
*/
|
|
8081
8115
|
const ServerProviderAvailability = Schema$1.Literals(["available", "unavailable"]);
|
|
8082
8116
|
const ServerProviderContinuation = Schema$1.Struct({ groupKey: TrimmedNonEmptyString });
|
|
8083
|
-
const ServerProviderVersionAdvisoryStatus =
|
|
8117
|
+
const ServerProviderVersionAdvisoryStatus = LiteralWithFallback([
|
|
8084
8118
|
"unknown",
|
|
8085
8119
|
"current",
|
|
8086
8120
|
"behind_latest"
|
|
8087
|
-
]);
|
|
8121
|
+
], "unknown");
|
|
8088
8122
|
const ServerProviderVersionAdvisory = Schema$1.Struct({
|
|
8089
8123
|
status: ServerProviderVersionAdvisoryStatus,
|
|
8090
8124
|
currentVersion: Schema$1.NullOr(TrimmedNonEmptyString),
|
|
@@ -8094,14 +8128,14 @@ const ServerProviderVersionAdvisory = Schema$1.Struct({
|
|
|
8094
8128
|
checkedAt: Schema$1.NullOr(IsoDateTime),
|
|
8095
8129
|
message: Schema$1.NullOr(TrimmedNonEmptyString)
|
|
8096
8130
|
});
|
|
8097
|
-
const ServerProviderUpdateStatus =
|
|
8131
|
+
const ServerProviderUpdateStatus = LiteralWithFallback([
|
|
8098
8132
|
"idle",
|
|
8099
8133
|
"queued",
|
|
8100
8134
|
"running",
|
|
8101
8135
|
"succeeded",
|
|
8102
8136
|
"failed",
|
|
8103
8137
|
"unchanged"
|
|
8104
|
-
]);
|
|
8138
|
+
], "idle");
|
|
8105
8139
|
const ServerProviderUpdateState = Schema$1.Struct({
|
|
8106
8140
|
status: ServerProviderUpdateStatus,
|
|
8107
8141
|
startedAt: Schema$1.NullOr(IsoDateTime),
|
|
@@ -8141,7 +8175,7 @@ const ServerProvider = Schema$1.Struct({
|
|
|
8141
8175
|
versionAdvisory: Schema$1.optionalKey(ServerProviderVersionAdvisory),
|
|
8142
8176
|
updateState: Schema$1.optionalKey(ServerProviderUpdateState)
|
|
8143
8177
|
});
|
|
8144
|
-
const ServerProviders =
|
|
8178
|
+
const ServerProviders = ForwardCompatibleArray(ServerProvider);
|
|
8145
8179
|
/**
|
|
8146
8180
|
* Treat the optional `availability` as "available" when absent. This is
|
|
8147
8181
|
* the rule legacy producers (which omit the field) and new producers
|
|
@@ -8311,7 +8345,7 @@ const ServerConfig = Schema$1.Struct({
|
|
|
8311
8345
|
keybindings: ResolvedKeybindingsConfig,
|
|
8312
8346
|
issues: ServerConfigIssues,
|
|
8313
8347
|
providers: ServerProviders,
|
|
8314
|
-
availableEditors:
|
|
8348
|
+
availableEditors: ForwardCompatibleArray(EditorId),
|
|
8315
8349
|
observability: ServerObservability,
|
|
8316
8350
|
settings: ServerSettings,
|
|
8317
8351
|
/** Whether shell subscriptions can emit an opt-in catch-up completion marker. */
|
|
@@ -13030,6 +13064,136 @@ const make$80 = Effect.gen(function* () {
|
|
|
13030
13064
|
});
|
|
13031
13065
|
const layer$72 = Layer.effect(PairingGrantStore, make$80).pipe(Layer.provideMerge(layer$73));
|
|
13032
13066
|
//#endregion
|
|
13067
|
+
//#region src/persistence/DatabaseSnapshot.ts
|
|
13068
|
+
/**
|
|
13069
|
+
* DatabaseSnapshot - pre-migration snapshot of the SQLite database.
|
|
13070
|
+
*
|
|
13071
|
+
* A failed migration used to be unrecoverable: the layer that runs migrations
|
|
13072
|
+
* is built during startup, so a migration that throws exits the process, the
|
|
13073
|
+
* supervisor (launchd KeepAlive, systemd Restart=always) starts the same broken
|
|
13074
|
+
* build again, and `effect_sql_migrations` has already advanced past what the
|
|
13075
|
+
* previous release understands - so reinstalling the old CLI does not recover
|
|
13076
|
+
* it either.
|
|
13077
|
+
*
|
|
13078
|
+
* This module gives the migration runner something to fall back to. Before a
|
|
13079
|
+
* boot that has migrations to apply, the database is snapshotted with SQLite's
|
|
13080
|
+
* own `VACUUM INTO`, which writes one consistent file without needing the WAL
|
|
13081
|
+
* and shared-memory sidecars. If the migration then fails, a marker is written
|
|
13082
|
+
* and the process exits; the *next* boot restores the snapshot before the
|
|
13083
|
+
* database is opened.
|
|
13084
|
+
*
|
|
13085
|
+
* Restoring on the next boot rather than in the failing process is what makes
|
|
13086
|
+
* this survive a power cut: the marker is written before any live file is
|
|
13087
|
+
* touched, and the restore is idempotent, so an interrupted restore simply
|
|
13088
|
+
* resumes.
|
|
13089
|
+
*
|
|
13090
|
+
* @module DatabaseSnapshot
|
|
13091
|
+
*/
|
|
13092
|
+
/** SQLite spreads a live database across the main file plus these sidecars. */
|
|
13093
|
+
const DB_SIDECAR_SUFFIXES = ["-wal", "-shm"];
|
|
13094
|
+
const BACKUP_DIR_NAME = "db-backup";
|
|
13095
|
+
const SNAPSHOT_FILE_NAME = "state.sqlite";
|
|
13096
|
+
const STAGING_FILE_NAME = "state.sqlite.staging";
|
|
13097
|
+
const RESTORE_MARKER_FILE_NAME = ".restore-pending";
|
|
13098
|
+
/**
|
|
13099
|
+
* One snapshot slot, beside the database it protects.
|
|
13100
|
+
*
|
|
13101
|
+
* Beside rather than under `<baseDir>/runtime` so a snapshot always lands on
|
|
13102
|
+
* the same filesystem as the database - `rename` is only atomic within one -
|
|
13103
|
+
* and so a dev run under `<baseDir>/dev` can never restore over live state.
|
|
13104
|
+
*/
|
|
13105
|
+
const databaseSnapshotPaths = Effect.fn("databaseSnapshotPaths")(function* (dbPath) {
|
|
13106
|
+
const path = yield* Path.Path;
|
|
13107
|
+
const backupDir = path.join(path.dirname(dbPath), BACKUP_DIR_NAME);
|
|
13108
|
+
return {
|
|
13109
|
+
backupDir,
|
|
13110
|
+
snapshotPath: path.join(backupDir, SNAPSHOT_FILE_NAME),
|
|
13111
|
+
stagingPath: path.join(backupDir, STAGING_FILE_NAME),
|
|
13112
|
+
markerPath: path.join(backupDir, RESTORE_MARKER_FILE_NAME)
|
|
13113
|
+
};
|
|
13114
|
+
});
|
|
13115
|
+
/**
|
|
13116
|
+
* fsync a path so the rename or copy above it survives a power cut. Directory
|
|
13117
|
+
* handles cannot be opened for writing, so both files and directories are
|
|
13118
|
+
* opened read-only and synced through the descriptor.
|
|
13119
|
+
*/
|
|
13120
|
+
const syncPath = (target) => Effect.scoped(Effect.gen(function* () {
|
|
13121
|
+
yield* (yield* (yield* FileSystem.FileSystem).open(target, { flag: "r" })).sync;
|
|
13122
|
+
}));
|
|
13123
|
+
/**
|
|
13124
|
+
* Snapshot the database unless one is already there.
|
|
13125
|
+
*
|
|
13126
|
+
* Never overwrites: after a failed migration the live database may already
|
|
13127
|
+
* carry half of that migration, and the existing snapshot is the only copy of
|
|
13128
|
+
* the schema the previous release can still read.
|
|
13129
|
+
*/
|
|
13130
|
+
const captureDatabaseSnapshot = Effect.fn("captureDatabaseSnapshot")(function* (dbPath) {
|
|
13131
|
+
const fs = yield* FileSystem.FileSystem;
|
|
13132
|
+
const sql = yield* SqlClient.SqlClient;
|
|
13133
|
+
const paths = yield* databaseSnapshotPaths(dbPath);
|
|
13134
|
+
if (yield* fs.exists(paths.snapshotPath)) return;
|
|
13135
|
+
yield* fs.makeDirectory(paths.backupDir, { recursive: true });
|
|
13136
|
+
yield* fs.remove(paths.stagingPath, { force: true });
|
|
13137
|
+
yield* sql`VACUUM INTO ${paths.stagingPath}`;
|
|
13138
|
+
yield* syncPath(paths.stagingPath);
|
|
13139
|
+
yield* fs.rename(paths.stagingPath, paths.snapshotPath);
|
|
13140
|
+
yield* syncPath(paths.backupDir);
|
|
13141
|
+
});
|
|
13142
|
+
/**
|
|
13143
|
+
* Record that the live database must be replaced by the snapshot.
|
|
13144
|
+
*
|
|
13145
|
+
* Written before anything touches a live file, so a crash between here and the
|
|
13146
|
+
* restore leaves the next boot able to tell that the database is not trusted.
|
|
13147
|
+
*/
|
|
13148
|
+
const markDatabaseRestorePending = Effect.fn("markDatabaseRestorePending")(function* (dbPath) {
|
|
13149
|
+
const fs = yield* FileSystem.FileSystem;
|
|
13150
|
+
const paths = yield* databaseSnapshotPaths(dbPath);
|
|
13151
|
+
if (yield* fs.exists(paths.markerPath)) return;
|
|
13152
|
+
yield* fs.makeDirectory(paths.backupDir, { recursive: true });
|
|
13153
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
13154
|
+
yield* (yield* fs.open(paths.markerPath, { flag: "wx" })).sync;
|
|
13155
|
+
}));
|
|
13156
|
+
yield* syncPath(paths.backupDir);
|
|
13157
|
+
});
|
|
13158
|
+
const databaseRestorePending = Effect.fn("databaseRestorePending")(function* (dbPath) {
|
|
13159
|
+
const fs = yield* FileSystem.FileSystem;
|
|
13160
|
+
const paths = yield* databaseSnapshotPaths(dbPath);
|
|
13161
|
+
return yield* fs.exists(paths.markerPath);
|
|
13162
|
+
});
|
|
13163
|
+
/**
|
|
13164
|
+
* Put the snapshot back, if one is pending. Call before the database is opened.
|
|
13165
|
+
*
|
|
13166
|
+
* Idempotent by construction: the marker is cleared only after the snapshot is
|
|
13167
|
+
* fully copied and synced, so an interrupted restore repeats harmlessly on the
|
|
13168
|
+
* next boot rather than leaving a half-copied database in place.
|
|
13169
|
+
*/
|
|
13170
|
+
const restoreDatabaseSnapshotIfPending = Effect.fn("restoreDatabaseSnapshotIfPending")(function* (dbPath) {
|
|
13171
|
+
const fs = yield* FileSystem.FileSystem;
|
|
13172
|
+
const path = yield* Path.Path;
|
|
13173
|
+
const paths = yield* databaseSnapshotPaths(dbPath);
|
|
13174
|
+
if (!(yield* fs.exists(paths.markerPath))) return false;
|
|
13175
|
+
if (!(yield* fs.exists(paths.snapshotPath))) {
|
|
13176
|
+
yield* fs.remove(paths.markerPath, { force: true });
|
|
13177
|
+
return false;
|
|
13178
|
+
}
|
|
13179
|
+
yield* fs.copyFile(paths.snapshotPath, dbPath);
|
|
13180
|
+
yield* syncPath(dbPath);
|
|
13181
|
+
for (const suffix of DB_SIDECAR_SUFFIXES) yield* fs.remove(`${dbPath}${suffix}`, { force: true });
|
|
13182
|
+
yield* syncPath(path.dirname(dbPath));
|
|
13183
|
+
yield* fs.remove(paths.markerPath, { force: true });
|
|
13184
|
+
yield* fs.remove(paths.snapshotPath, { force: true });
|
|
13185
|
+
yield* syncPath(paths.backupDir);
|
|
13186
|
+
return true;
|
|
13187
|
+
});
|
|
13188
|
+
/** Drop a snapshot the migrations no longer need. */
|
|
13189
|
+
const discardDatabaseSnapshot = Effect.fn("discardDatabaseSnapshot")(function* (dbPath) {
|
|
13190
|
+
const fs = yield* FileSystem.FileSystem;
|
|
13191
|
+
const paths = yield* databaseSnapshotPaths(dbPath);
|
|
13192
|
+
yield* fs.remove(paths.markerPath, { force: true });
|
|
13193
|
+
yield* fs.remove(paths.snapshotPath, { force: true });
|
|
13194
|
+
yield* fs.remove(paths.stagingPath, { force: true });
|
|
13195
|
+
});
|
|
13196
|
+
//#endregion
|
|
13033
13197
|
//#region src/persistence/Migrations/001_OrchestrationEvents.ts
|
|
13034
13198
|
var _001_OrchestrationEvents_default = Effect.gen(function* () {
|
|
13035
13199
|
const sql = yield* SqlClient.SqlClient;
|
|
@@ -14922,6 +15086,59 @@ const runMigrations = Effect.fn("runMigrations")(function* ({ toMigrationInclusi
|
|
|
14922
15086
|
yield* migrations.length === 0 ? Effect.logDebug("Database schema is current") : Effect.log("Migrations ran successfully").pipe(Effect.annotateLogs({ migrations }));
|
|
14923
15087
|
return executedMigrations;
|
|
14924
15088
|
});
|
|
15089
|
+
/**
|
|
15090
|
+
* Highest migration already recorded in the database, or 0 for a database that
|
|
15091
|
+
* has never been migrated.
|
|
15092
|
+
*
|
|
15093
|
+
* Read from `sqlite_master` first because the tracking table does not exist on
|
|
15094
|
+
* a first boot, and a missing-table error here would be indistinguishable from
|
|
15095
|
+
* a real failure.
|
|
15096
|
+
*/
|
|
15097
|
+
const latestAppliedMigrationId = Effect.fn("latestAppliedMigrationId")(function* () {
|
|
15098
|
+
const sql = yield* SqlClient.SqlClient;
|
|
15099
|
+
if ((yield* sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'effect_sql_migrations'`).length === 0) return 0;
|
|
15100
|
+
return (yield* sql`SELECT max(migration_id) AS id FROM effect_sql_migrations`)[0]?.id ?? 0;
|
|
15101
|
+
});
|
|
15102
|
+
/**
|
|
15103
|
+
* Run migrations with a snapshot to fall back to.
|
|
15104
|
+
*
|
|
15105
|
+
* Only a boot that actually has migrations to apply pays for the snapshot, so a
|
|
15106
|
+
* normal restart still opens the database and serves immediately.
|
|
15107
|
+
*
|
|
15108
|
+
* On failure the snapshot is deliberately *not* restored here: the database is
|
|
15109
|
+
* still open on this connection, and a restore under an open connection is how
|
|
15110
|
+
* a half-copied file gets read. The marker written instead makes the next boot
|
|
15111
|
+
* restore it before anything opens the database, which is also what makes an
|
|
15112
|
+
* interrupted restore resume rather than corrupt.
|
|
15113
|
+
*/
|
|
15114
|
+
const runGuardedMigrations = Effect.fn("runGuardedMigrations")(function* (input) {
|
|
15115
|
+
const appliedId = yield* latestAppliedMigrationId();
|
|
15116
|
+
const throughId = input.options?.toMigrationInclusive;
|
|
15117
|
+
const targetId = migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).reduce((highest, [id]) => id > highest ? id : highest, 0);
|
|
15118
|
+
if (targetId <= appliedId) {
|
|
15119
|
+
yield* discardSnapshotQuietly(input.dbPath);
|
|
15120
|
+
return yield* runMigrations(input.options);
|
|
15121
|
+
}
|
|
15122
|
+
yield* Effect.log("Snapshotting the database before migrations").pipe(Effect.annotateLogs({
|
|
15123
|
+
appliedMigrationId: appliedId,
|
|
15124
|
+
targetMigrationId: targetId
|
|
15125
|
+
}));
|
|
15126
|
+
yield* captureDatabaseSnapshot(input.dbPath).pipe(Effect.tapCause((cause) => Effect.logError("Refusing to migrate without a database snapshot").pipe(Effect.annotateLogs({
|
|
15127
|
+
dbPath: input.dbPath,
|
|
15128
|
+
cause
|
|
15129
|
+
}))));
|
|
15130
|
+
const executed = yield* runMigrations(input.options).pipe(Effect.tapCause(() => markDatabaseRestorePending(input.dbPath).pipe(Effect.andThen(Effect.logError("Migration failed; the database will be restored on the next start").pipe(Effect.annotateLogs({
|
|
15131
|
+
appliedMigrationId: appliedId,
|
|
15132
|
+
targetMigrationId: targetId
|
|
15133
|
+
}))), Effect.tapError((cause) => Effect.logError("Could not mark the database for restore").pipe(Effect.annotateLogs({ cause }))), Effect.ignore)));
|
|
15134
|
+
yield* discardSnapshotQuietly(input.dbPath);
|
|
15135
|
+
return executed;
|
|
15136
|
+
});
|
|
15137
|
+
/**
|
|
15138
|
+
* A snapshot that cannot be deleted is wasted disk, not a reason to refuse to
|
|
15139
|
+
* start - the schema it protected is already applied by the time this runs.
|
|
15140
|
+
*/
|
|
15141
|
+
const discardSnapshotQuietly = (dbPath) => discardDatabaseSnapshot(dbPath).pipe(Effect.tapError((cause) => Effect.logWarning("Could not discard the database snapshot").pipe(Effect.annotateLogs({ cause }))), Effect.ignore);
|
|
14925
15142
|
Layer.effectDiscard(runMigrations());
|
|
14926
15143
|
//#endregion
|
|
14927
15144
|
//#region src/persistence/Layers/Sqlite.ts
|
|
@@ -14934,17 +15151,27 @@ const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* (co
|
|
|
14934
15151
|
const loader = defaultSqliteClientLoaders[runtime];
|
|
14935
15152
|
return (yield* Effect.promise(loader)).layer(config);
|
|
14936
15153
|
}, Layer.unwrap);
|
|
14937
|
-
const
|
|
15154
|
+
const applyPragmas = Effect.gen(function* () {
|
|
14938
15155
|
const sql = yield* SqlClient.SqlClient;
|
|
14939
15156
|
yield* sql`PRAGMA journal_mode = WAL;`;
|
|
14940
15157
|
yield* sql`PRAGMA foreign_keys = ON;`;
|
|
14941
|
-
|
|
14942
|
-
}));
|
|
15158
|
+
});
|
|
15159
|
+
const setupFile = (dbPath) => Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runGuardedMigrations({ dbPath }))));
|
|
15160
|
+
/**
|
|
15161
|
+
* The in-memory database used by tests has no file to snapshot and nothing that
|
|
15162
|
+
* survives the process to restore.
|
|
15163
|
+
*
|
|
15164
|
+
* A separate layer rather than a nullable path, so the memory setup keeps
|
|
15165
|
+
* needing nothing but `SqlClient`: sharing one function would put the file
|
|
15166
|
+
* path's `FileSystem | Path` requirement into every test layer built on it.
|
|
15167
|
+
*/
|
|
15168
|
+
const setupMemory = Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runMigrations())));
|
|
14943
15169
|
const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(function* (dbPath) {
|
|
14944
15170
|
const fs = yield* FileSystem.FileSystem;
|
|
14945
15171
|
const path = yield* Path.Path;
|
|
14946
15172
|
yield* fs.makeDirectory(path.dirname(dbPath), { recursive: true });
|
|
14947
|
-
|
|
15173
|
+
yield* restoreDatabaseSnapshotIfPending(dbPath).pipe(Effect.tap((restored) => restored ? Effect.logWarning("Restored the database snapshot left by a failed migration") : Effect.void));
|
|
15174
|
+
return Layer.provideMerge(setupFile(dbPath), makeRuntimeSqliteLayer({
|
|
14948
15175
|
filename: dbPath,
|
|
14949
15176
|
spanAttributes: {
|
|
14950
15177
|
"db.name": path.basename(dbPath),
|
|
@@ -14952,7 +15179,7 @@ const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(functio
|
|
|
14952
15179
|
}
|
|
14953
15180
|
}));
|
|
14954
15181
|
}, Layer.unwrap);
|
|
14955
|
-
Layer.provideMerge(
|
|
15182
|
+
Layer.provideMerge(setupMemory, makeRuntimeSqliteLayer({ filename: ":memory:" }));
|
|
14956
15183
|
const layerConfig = Layer.unwrap(Effect.map(Effect.service(ServerConfig$1), ({ dbPath }) => makeSqlitePersistenceLive(dbPath)));
|
|
14957
15184
|
const INTERNAL_ADMINISTRATIVE_BOOTSTRAP_SUBJECT = "administrative-bootstrap";
|
|
14958
15185
|
const serverAuthInternalErrorContext = { cause: Schema$1.Defect() };
|
|
@@ -29913,11 +30140,14 @@ const make$64 = Effect.fn("cloud.boot_service.make")(function* (input) {
|
|
|
29913
30140
|
baseDir: input.baseDir
|
|
29914
30141
|
};
|
|
29915
30142
|
}).pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })), Effect.withSpan("cloud.boot_service.status"));
|
|
30143
|
+
const readUnit = fs.exists(unitPath).pipe(Effect.flatMap((exists) => exists ? fs.readFileString(unitPath).pipe(Effect.map(Option.some)) : Effect.succeed(Option.none())), Effect.mapError((cause) => new BootServiceInstallError({ cause })), Effect.withSpan("cloud.boot_service.read_unit"));
|
|
29916
30144
|
return BootService.of({
|
|
29917
30145
|
install,
|
|
29918
30146
|
uninstall,
|
|
29919
30147
|
restart,
|
|
29920
30148
|
status,
|
|
30149
|
+
readUnit,
|
|
30150
|
+
revertUnit: rollbackFailedInstall,
|
|
29921
30151
|
logPath
|
|
29922
30152
|
});
|
|
29923
30153
|
});
|
|
@@ -31570,7 +31800,15 @@ const PersistedServerRuntimeState = Schema$1.Struct({
|
|
|
31570
31800
|
host: Schema$1.optional(Schema$1.String),
|
|
31571
31801
|
port: Schema$1.Int,
|
|
31572
31802
|
origin: Schema$1.String,
|
|
31573
|
-
startedAt: Schema$1.String
|
|
31803
|
+
startedAt: Schema$1.String,
|
|
31804
|
+
/**
|
|
31805
|
+
* Which build wrote this. `p4c service update` restarts the service and then
|
|
31806
|
+
* has to tell whether what came back is the new build or the old one still
|
|
31807
|
+
* running - the pid alone cannot say that.
|
|
31808
|
+
*
|
|
31809
|
+
* Optional so a file written by an older server still decodes.
|
|
31810
|
+
*/
|
|
31811
|
+
cliVersion: Schema$1.optional(Schema$1.String)
|
|
31574
31812
|
});
|
|
31575
31813
|
var ServerRuntimeStateError = class extends Schema$1.TaggedErrorClass()("ServerRuntimeStateError", {
|
|
31576
31814
|
operation: Schema$1.Literals([
|
|
@@ -31596,7 +31834,8 @@ const makePersistedServerRuntimeState = (input) => Effect.map(DateTime.now, (now
|
|
|
31596
31834
|
...input.config.host ? { host: input.config.host } : {},
|
|
31597
31835
|
port: input.port,
|
|
31598
31836
|
origin: runtimeOriginForConfig(input.config, input.port),
|
|
31599
|
-
startedAt: DateTime.formatIso(now)
|
|
31837
|
+
startedAt: DateTime.formatIso(now),
|
|
31838
|
+
cliVersion: version
|
|
31600
31839
|
}));
|
|
31601
31840
|
const persistServerRuntimeState = (input) => writeFileStringAtomically({
|
|
31602
31841
|
filePath: input.path,
|
|
@@ -33014,7 +33253,8 @@ const ICON_SOURCE_FILES = [
|
|
|
33014
33253
|
"src/index.html"
|
|
33015
33254
|
];
|
|
33016
33255
|
const LINK_ICON_HTML_RE = /<link\b(?=[^>]*\brel=["'](?:icon|shortcut icon)["'])(?=[^>]*\bhref=["']([^"'?]+))[^>]*>/i;
|
|
33017
|
-
const
|
|
33256
|
+
const ICON_REL_RE = /\brel\s*:\s*["'](?:icon|shortcut icon)["']/i;
|
|
33257
|
+
const ICON_HREF_RE = /\bhref\s*:\s*["']([^"'?]+)/i;
|
|
33018
33258
|
var ProjectFaviconResolutionError = class extends Schema$1.TaggedErrorClass()("ProjectFaviconResolutionError", {
|
|
33019
33259
|
operation: Schema$1.Literals([
|
|
33020
33260
|
"normalize-workspace",
|
|
@@ -33036,8 +33276,11 @@ var ProjectFaviconResolver = class extends Context.Service()("@p4code/cli/projec
|
|
|
33036
33276
|
function extractIconHref(source) {
|
|
33037
33277
|
const htmlMatch = source.match(LINK_ICON_HTML_RE);
|
|
33038
33278
|
if (htmlMatch?.[1]) return htmlMatch[1];
|
|
33039
|
-
const
|
|
33040
|
-
|
|
33279
|
+
for (const run of source.split("}")) {
|
|
33280
|
+
if (!ICON_REL_RE.test(run)) continue;
|
|
33281
|
+
const hrefMatch = run.match(ICON_HREF_RE);
|
|
33282
|
+
if (hrefMatch?.[1]) return hrefMatch[1];
|
|
33283
|
+
}
|
|
33041
33284
|
return null;
|
|
33042
33285
|
}
|
|
33043
33286
|
const optionOnNotFound$1 = (effect) => effect.pipe(Effect.map(Option.some), Effect.catchTags({ PlatformError: (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : Effect.fail(error) }));
|
|
@@ -33942,7 +34185,16 @@ Object.freeze({
|
|
|
33942
34185
|
fromEnd: 0
|
|
33943
34186
|
});
|
|
33944
34187
|
//#endregion
|
|
33945
|
-
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.
|
|
34188
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.5_patch_hash=ab83d1300a2b500057de1efef105e40b3766e14e1859523715f5205d_75eb9e5f35bdb1c1e149107bc7dcf802/node_modules/@pierre/diffs/dist/utils/getHunkSideBoundaries.js
|
|
34189
|
+
/** Converts a unified hunk side's start/count into its consumed-file range. */
|
|
34190
|
+
function getHunkSideStartBoundary(start, count) {
|
|
34191
|
+
return start - (count === 0 ? 0 : 1);
|
|
34192
|
+
}
|
|
34193
|
+
function getHunkSideEndBoundary(start, count) {
|
|
34194
|
+
return getHunkSideStartBoundary(start, count) + count;
|
|
34195
|
+
}
|
|
34196
|
+
//#endregion
|
|
34197
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.5_patch_hash=ab83d1300a2b500057de1efef105e40b3766e14e1859523715f5205d_75eb9e5f35bdb1c1e149107bc7dcf802/node_modules/@pierre/diffs/dist/utils/cleanLastNewline.js
|
|
33946
34198
|
function cleanLastNewline(contents) {
|
|
33947
34199
|
let end = contents.length;
|
|
33948
34200
|
if (contents.charCodeAt(end - 1) === 10) {
|
|
@@ -33952,7 +34204,7 @@ function cleanLastNewline(contents) {
|
|
|
33952
34204
|
return contents.slice(0, end);
|
|
33953
34205
|
}
|
|
33954
34206
|
//#endregion
|
|
33955
|
-
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.
|
|
34207
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.5_patch_hash=ab83d1300a2b500057de1efef105e40b3766e14e1859523715f5205d_75eb9e5f35bdb1c1e149107bc7dcf802/node_modules/@pierre/diffs/dist/utils/detachString.js
|
|
33956
34208
|
const stringDetachEncoder = new TextEncoder();
|
|
33957
34209
|
const stringDetachDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
33958
34210
|
const SURROGATE_CODE_UNIT_PATTERN = /[\uD800-\uDFFF]/;
|
|
@@ -33970,7 +34222,152 @@ function detachString(value) {
|
|
|
33970
34222
|
return stringDetachDecoder.decode(stringDetachBuffer.subarray(0, written));
|
|
33971
34223
|
}
|
|
33972
34224
|
//#endregion
|
|
33973
|
-
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.
|
|
34225
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.5_patch_hash=ab83d1300a2b500057de1efef105e40b3766e14e1859523715f5205d_75eb9e5f35bdb1c1e149107bc7dcf802/node_modules/@pierre/diffs/dist/utils/realignChangeContent.js
|
|
34226
|
+
const MAX_ALIGNMENT_COMPARISONS = 4096;
|
|
34227
|
+
const MIN_IMPROVEMENT_PER_PAIR = .5;
|
|
34228
|
+
/**
|
|
34229
|
+
* Re-split count-mismatched change blocks in every hunk so paired lines are
|
|
34230
|
+
* chosen by content similarity instead of position, then slide blank-line
|
|
34231
|
+
* insert/delete blocks to the top of their blank run. Mutates `hunks` in
|
|
34232
|
+
* place; rendered row counts are unchanged (a split block covers the same
|
|
34233
|
+
* split/unified rows as the original).
|
|
34234
|
+
*/
|
|
34235
|
+
function realignChangeContentBySimilarity(diff) {
|
|
34236
|
+
for (const hunk of diff.hunks) {
|
|
34237
|
+
for (let index = 0; index < hunk.hunkContent.length; index++) {
|
|
34238
|
+
const content = hunk.hunkContent[index];
|
|
34239
|
+
if (content.type !== "change") continue;
|
|
34240
|
+
const replacement = realignChangeBlock(diff, content);
|
|
34241
|
+
if (replacement != null) {
|
|
34242
|
+
hunk.hunkContent.splice(index, 1, ...replacement);
|
|
34243
|
+
index += replacement.length - 1;
|
|
34244
|
+
}
|
|
34245
|
+
}
|
|
34246
|
+
slideBlankBoundaryBlocksUp(hunk, diff);
|
|
34247
|
+
}
|
|
34248
|
+
}
|
|
34249
|
+
/**
|
|
34250
|
+
* Slide pure insert/delete blocks made entirely of blank lines to the top of
|
|
34251
|
+
* the blank run they sit in. Adding or removing a blank line next to
|
|
34252
|
+
* existing blanks is ambiguous, and the diff library reports the change at
|
|
34253
|
+
* the run's bottom — so pressing Enter at the end of a line marks a blank
|
|
34254
|
+
* *below* the caret as inserted while the caret's own new line renders as
|
|
34255
|
+
* context. Sliding up anchors the change to the content above it (the caret
|
|
34256
|
+
* line after an Enter) instead.
|
|
34257
|
+
*
|
|
34258
|
+
* The slide is all-or-nothing: it only applies when the block comes to rest
|
|
34259
|
+
* directly beneath remaining in-hunk content. A slide that would consume the
|
|
34260
|
+
* hunk's entire leading context was stopped by the hunk's edge — a context
|
|
34261
|
+
* window cut, not the top of the blank run — and that landing spot is
|
|
34262
|
+
* arbitrary, so the block keeps the library's bottom-of-run anchor (which
|
|
34263
|
+
* sits against the content below the run). Non-blank blocks never slide, so
|
|
34264
|
+
* code that merely ends like its neighbor (an added function before an
|
|
34265
|
+
* identical `}`) keeps the library's canonical position.
|
|
34266
|
+
*/
|
|
34267
|
+
function slideBlankBoundaryBlocksUp(hunk, diff) {
|
|
34268
|
+
const { hunkContent } = hunk;
|
|
34269
|
+
for (let index = 1; index < hunkContent.length; index++) {
|
|
34270
|
+
const block = hunkContent[index];
|
|
34271
|
+
const previous = hunkContent[index - 1];
|
|
34272
|
+
if (block.type !== "change" || block.additions > 0 && block.deletions > 0 || previous.type !== "context") continue;
|
|
34273
|
+
const isInsert = block.additions > 0;
|
|
34274
|
+
const lines = isInsert ? diff.additionLines : diff.deletionLines;
|
|
34275
|
+
const blockStart = isInsert ? block.additionLineIndex : block.deletionLineIndex;
|
|
34276
|
+
const blockLength = isInsert ? block.additions : block.deletions;
|
|
34277
|
+
const unit = lines[blockStart] ?? "";
|
|
34278
|
+
if (unit.trim() !== "") continue;
|
|
34279
|
+
let uniform = true;
|
|
34280
|
+
for (let offset = 1; offset < blockLength; offset++) if (lines[blockStart + offset] !== unit) {
|
|
34281
|
+
uniform = false;
|
|
34282
|
+
break;
|
|
34283
|
+
}
|
|
34284
|
+
if (!uniform) continue;
|
|
34285
|
+
let slide = 0;
|
|
34286
|
+
while (slide < previous.lines && diff.additionLines[previous.additionLineIndex + previous.lines - 1 - slide] === unit) slide++;
|
|
34287
|
+
if (slide === 0) continue;
|
|
34288
|
+
if (index === 1 && slide === previous.lines) continue;
|
|
34289
|
+
block.additionLineIndex -= slide;
|
|
34290
|
+
block.deletionLineIndex -= slide;
|
|
34291
|
+
const blockAdditionEnd = block.additionLineIndex + block.additions;
|
|
34292
|
+
const blockDeletionEnd = block.deletionLineIndex + block.deletions;
|
|
34293
|
+
const next = hunkContent[index + 1];
|
|
34294
|
+
if (next?.type === "context") {
|
|
34295
|
+
next.lines += slide;
|
|
34296
|
+
next.additionLineIndex = blockAdditionEnd;
|
|
34297
|
+
next.deletionLineIndex = blockDeletionEnd;
|
|
34298
|
+
} else hunkContent.splice(index + 1, 0, {
|
|
34299
|
+
type: "context",
|
|
34300
|
+
lines: slide,
|
|
34301
|
+
additionLineIndex: blockAdditionEnd,
|
|
34302
|
+
deletionLineIndex: blockDeletionEnd
|
|
34303
|
+
});
|
|
34304
|
+
previous.lines -= slide;
|
|
34305
|
+
if (previous.lines === 0) {
|
|
34306
|
+
hunkContent.splice(index - 1, 1);
|
|
34307
|
+
index--;
|
|
34308
|
+
}
|
|
34309
|
+
}
|
|
34310
|
+
}
|
|
34311
|
+
function realignChangeBlock(diff, content) {
|
|
34312
|
+
const { deletions, additions, deletionLineIndex, additionLineIndex } = content;
|
|
34313
|
+
const pairCount = Math.min(deletions, additions);
|
|
34314
|
+
const surplus = Math.abs(additions - deletions);
|
|
34315
|
+
if (pairCount === 0 || surplus === 0 || pairCount * (surplus + 1) > MAX_ALIGNMENT_COMPARISONS) return null;
|
|
34316
|
+
const strippedDeletions = [];
|
|
34317
|
+
for (let line = 0; line < deletions; line++) strippedDeletions.push(stripWhitespace(diff.deletionLines[deletionLineIndex + line] ?? ""));
|
|
34318
|
+
const strippedAdditions = [];
|
|
34319
|
+
for (let line = 0; line < additions; line++) strippedAdditions.push(stripWhitespace(diff.additionLines[additionLineIndex + line] ?? ""));
|
|
34320
|
+
const additionsAreLonger = additions > deletions;
|
|
34321
|
+
let bestOffset = 0;
|
|
34322
|
+
let bestScore = -1;
|
|
34323
|
+
for (let offset = 0; offset <= surplus; offset++) {
|
|
34324
|
+
let score = 0;
|
|
34325
|
+
for (let pair = 0; pair < pairCount; pair++) score += lineSimilarity(strippedDeletions[pair + (additionsAreLonger ? 0 : offset)], strippedAdditions[pair + (additionsAreLonger ? offset : 0)]);
|
|
34326
|
+
if (offset === 0) bestScore = score + pairCount * MIN_IMPROVEMENT_PER_PAIR;
|
|
34327
|
+
else if (score > bestScore) {
|
|
34328
|
+
bestScore = score;
|
|
34329
|
+
bestOffset = offset;
|
|
34330
|
+
}
|
|
34331
|
+
}
|
|
34332
|
+
if (bestOffset === 0) return null;
|
|
34333
|
+
const blocks = [];
|
|
34334
|
+
const pushBlock = (blockDeletions, blockAdditions, blockDeletionIndex, blockAdditionIndex) => {
|
|
34335
|
+
if (blockDeletions > 0 || blockAdditions > 0) blocks.push({
|
|
34336
|
+
type: "change",
|
|
34337
|
+
deletions: blockDeletions,
|
|
34338
|
+
additions: blockAdditions,
|
|
34339
|
+
deletionLineIndex: blockDeletionIndex,
|
|
34340
|
+
additionLineIndex: blockAdditionIndex
|
|
34341
|
+
});
|
|
34342
|
+
};
|
|
34343
|
+
if (additionsAreLonger) {
|
|
34344
|
+
pushBlock(0, bestOffset, deletionLineIndex, additionLineIndex);
|
|
34345
|
+
pushBlock(pairCount, pairCount, deletionLineIndex, additionLineIndex + bestOffset);
|
|
34346
|
+
pushBlock(0, additions - pairCount - bestOffset, deletionLineIndex + pairCount, additionLineIndex + bestOffset + pairCount);
|
|
34347
|
+
} else {
|
|
34348
|
+
pushBlock(bestOffset, 0, deletionLineIndex, additionLineIndex);
|
|
34349
|
+
pushBlock(pairCount, pairCount, deletionLineIndex + bestOffset, additionLineIndex);
|
|
34350
|
+
pushBlock(deletions - pairCount - bestOffset, 0, deletionLineIndex + bestOffset + pairCount, additionLineIndex + pairCount);
|
|
34351
|
+
}
|
|
34352
|
+
return blocks;
|
|
34353
|
+
}
|
|
34354
|
+
const WHITESPACE = /\s+/g;
|
|
34355
|
+
function stripWhitespace(line) {
|
|
34356
|
+
return line.replace(WHITESPACE, "");
|
|
34357
|
+
}
|
|
34358
|
+
function lineSimilarity(a, b) {
|
|
34359
|
+
if (a === b) return 1;
|
|
34360
|
+
const maxLength = Math.max(a.length, b.length);
|
|
34361
|
+
const minLength = Math.min(a.length, b.length);
|
|
34362
|
+
if (minLength === 0) return 0;
|
|
34363
|
+
let prefix = 0;
|
|
34364
|
+
while (prefix < minLength && a[prefix] === b[prefix]) prefix++;
|
|
34365
|
+
let suffix = 0;
|
|
34366
|
+
while (suffix < minLength - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix]) suffix++;
|
|
34367
|
+
return (prefix + suffix) / maxLength;
|
|
34368
|
+
}
|
|
34369
|
+
//#endregion
|
|
34370
|
+
//#region ../../node_modules/.pnpm/@pierre+diffs@1.3.5_patch_hash=ab83d1300a2b500057de1efef105e40b3766e14e1859523715f5205d_75eb9e5f35bdb1c1e149107bc7dcf802/node_modules/@pierre/diffs/dist/utils/parsePatchFiles.js
|
|
33974
34371
|
function processPatch(data, cacheKeyPrefix, throwOnError) {
|
|
33975
34372
|
try {
|
|
33976
34373
|
return _processPatch(data, cacheKeyPrefix, throwOnError);
|
|
@@ -34046,9 +34443,9 @@ function _processFile(fileDiffString, { cacheKey, isGitDiff = GIT_DIFF_FILE_BREA
|
|
|
34046
34443
|
if (currentFile.deletionLines.length === 1 && oldFile?.contents === "") currentFile.deletionLines.length = 0;
|
|
34047
34444
|
for (const line of lines) {
|
|
34048
34445
|
if (line.startsWith("diff --git")) {
|
|
34049
|
-
const filenameMatch
|
|
34050
|
-
const prevName = filenameMatch
|
|
34051
|
-
const name = filenameMatch
|
|
34446
|
+
const filenameMatch = line.trim().match(ALTERNATE_FILE_NAMES_GIT);
|
|
34447
|
+
const prevName = filenameMatch?.[1] ?? filenameMatch?.[2];
|
|
34448
|
+
const name = filenameMatch?.[3] ?? filenameMatch?.[4];
|
|
34052
34449
|
if (prevName == null || name == null) {
|
|
34053
34450
|
if (throwOnError) throw Error("parsePatchContent: invalid git diff header");
|
|
34054
34451
|
else console.error("parsePatchContent: invalid git diff header", line);
|
|
@@ -34195,9 +34592,9 @@ function _processFile(fileDiffString, { cacheKey, isGitDiff = GIT_DIFF_FILE_BREA
|
|
|
34195
34592
|
if (throwOnError && (parsedAdditionLines !== hunkData.additionCount || parsedDeletionLines !== hunkData.deletionCount)) throw Error("parsePatchContent: hunk line count mismatch");
|
|
34196
34593
|
hunkData.additionLines = additionLines;
|
|
34197
34594
|
hunkData.deletionLines = deletionLines;
|
|
34198
|
-
hunkData.collapsedBefore = Math.max(hunkData.additionStart
|
|
34595
|
+
hunkData.collapsedBefore = Math.max(getHunkSideStartBoundary(hunkData.additionStart, hunkData.additionCount) - lastHunkEnd, 0);
|
|
34199
34596
|
currentFile.hunks.push(hunkData);
|
|
34200
|
-
lastHunkEnd = hunkData.additionStart
|
|
34597
|
+
lastHunkEnd = getHunkSideEndBoundary(hunkData.additionStart, hunkData.additionCount);
|
|
34201
34598
|
for (const content of hunkData.hunkContent) if (content.type === "context") {
|
|
34202
34599
|
hunkData.splitLineCount += content.lines;
|
|
34203
34600
|
hunkData.unifiedLineCount += content.lines;
|
|
@@ -34214,9 +34611,9 @@ function _processFile(fileDiffString, { cacheKey, isGitDiff = GIT_DIFF_FILE_BREA
|
|
|
34214
34611
|
if (throwOnError && isPartial && !isGitDiff && currentFile.hunks.length === 0) throw Error("parsePatchContent: unified file has no hunks");
|
|
34215
34612
|
if (currentFile.hunks.length > 0 && !isPartial && currentFile.additionLines.length > 0 && currentFile.deletionLines.length > 0) {
|
|
34216
34613
|
const lastHunk = currentFile.hunks[currentFile.hunks.length - 1];
|
|
34217
|
-
const lastHunkEnd
|
|
34614
|
+
const lastHunkEnd = getHunkSideEndBoundary(lastHunk.additionStart, lastHunk.additionCount);
|
|
34218
34615
|
const totalFileLines = currentFile.additionLines.length;
|
|
34219
|
-
const collapsedAfter = Math.max(totalFileLines - lastHunkEnd
|
|
34616
|
+
const collapsedAfter = Math.max(totalFileLines - lastHunkEnd, 0);
|
|
34220
34617
|
currentFile.splitLineCount += collapsedAfter;
|
|
34221
34618
|
currentFile.unifiedLineCount += collapsedAfter;
|
|
34222
34619
|
}
|
|
@@ -34227,6 +34624,7 @@ function _processFile(fileDiffString, { cacheKey, isGitDiff = GIT_DIFF_FILE_BREA
|
|
|
34227
34624
|
else if (oldFile != null && oldFile.contents !== "" && (newFile == null || newFile.contents === "")) currentFile.type = "deleted";
|
|
34228
34625
|
}
|
|
34229
34626
|
if (currentFile.type !== "rename-pure" && currentFile.type !== "rename-changed") currentFile.prevName = void 0;
|
|
34627
|
+
realignChangeContentBySimilarity(currentFile);
|
|
34230
34628
|
return currentFile;
|
|
34231
34629
|
}
|
|
34232
34630
|
/**
|
|
@@ -38884,7 +39282,7 @@ const LINEAR_ISSUE_FIELDS = [
|
|
|
38884
39282
|
* a Linear workspace is edited by people at human speed rather than by agents
|
|
38885
39283
|
* at machine speed.
|
|
38886
39284
|
*/
|
|
38887
|
-
const POLL_INTERVAL$
|
|
39285
|
+
const POLL_INTERVAL$2 = Duration.seconds(30);
|
|
38888
39286
|
/** Linear's cap on one page. Asking for more is an error, not a bigger page. */
|
|
38889
39287
|
const LINEAR_PAGE_LIMIT = 250;
|
|
38890
39288
|
const LINEAR_LIST_TOOL = "list_issues";
|
|
@@ -39136,7 +39534,7 @@ const makeLinearTaskRepository = Effect.gen(function* () {
|
|
|
39136
39534
|
taskId
|
|
39137
39535
|
});
|
|
39138
39536
|
}).pipe(Effect.ignoreCause({ log: true }));
|
|
39139
|
-
yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL$
|
|
39537
|
+
yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL$2)))).pipe(Effect.forkScoped);
|
|
39140
39538
|
return {
|
|
39141
39539
|
create,
|
|
39142
39540
|
upsert,
|
|
@@ -42121,7 +42519,7 @@ const COMMON_DEV_PORTS = Object.freeze([
|
|
|
42121
42519
|
8888,
|
|
42122
42520
|
9e3
|
|
42123
42521
|
]);
|
|
42124
|
-
const POLL_INTERVAL = Duration.seconds(3);
|
|
42522
|
+
const POLL_INTERVAL$1 = Duration.seconds(3);
|
|
42125
42523
|
const LSOF_TIMEOUT_MS = 5e3;
|
|
42126
42524
|
const WINDOWS_LISTENER_TIMEOUT_MS = 5e3;
|
|
42127
42525
|
const terminalOwnerKey = (owner) => `${owner.threadId}\u0000${owner.terminalId}`;
|
|
@@ -42296,7 +42694,7 @@ const make$46 = Effect.gen(function* PortDiscoveryMake() {
|
|
|
42296
42694
|
lastSnapshot: next
|
|
42297
42695
|
}])) yield* broadcast(next);
|
|
42298
42696
|
}, Effect.catchCause((cause) => Effect.logWarning("preview port scan failed", Cause.pretty(cause))));
|
|
42299
|
-
yield* Effect.forkScoped(pollTick().pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL))));
|
|
42697
|
+
yield* Effect.forkScoped(pollTick().pipe(Effect.repeat(Schedule.spaced(POLL_INTERVAL$1))));
|
|
42300
42698
|
const acquireRetention = Effect.fn("PortDiscovery.retain")(function* () {
|
|
42301
42699
|
if (yield* Ref.modify(stateRef, (state) => [state.retainCount === 0, {
|
|
42302
42700
|
...state,
|
|
@@ -63703,7 +64101,7 @@ function readScheduledWakeAt(input, completedAt) {
|
|
|
63703
64101
|
if (typeof delaySeconds !== "number" || !Number.isFinite(delaySeconds) || delaySeconds < 0) return;
|
|
63704
64102
|
const completedAtMs = Date.parse(completedAt);
|
|
63705
64103
|
if (Number.isNaN(completedAtMs)) return;
|
|
63706
|
-
return
|
|
64104
|
+
return DateTime.formatIso(DateTime.makeUnsafe(completedAtMs + delaySeconds * 1e3));
|
|
63707
64105
|
}
|
|
63708
64106
|
function isClaudeTaskTool(toolName) {
|
|
63709
64107
|
return toolName === "TaskCreate" || toolName === "TaskUpdate" || toolName === "TaskList";
|
|
@@ -103781,6 +104179,142 @@ const reconcileWithNewBinary = Effect.fn("cli.selfUpdate.reconcileWithNewBinary"
|
|
|
103781
104179
|
});
|
|
103782
104180
|
});
|
|
103783
104181
|
//#endregion
|
|
104182
|
+
//#region src/service/updateHealth.ts
|
|
104183
|
+
/**
|
|
104184
|
+
* Waiting for a restarted service to actually come back.
|
|
104185
|
+
*
|
|
104186
|
+
* `launchctl kickstart -k` returning 0 means launchd accepted the restart
|
|
104187
|
+
* request, not that the child booted; `systemctl restart` is the same. The
|
|
104188
|
+
* server already records where it is listening once its HTTP server is up, so
|
|
104189
|
+
* that file - written by the very process the update is about - is the honest
|
|
104190
|
+
* signal that the new build works.
|
|
104191
|
+
*
|
|
104192
|
+
* @module ServiceUpdateHealth
|
|
104193
|
+
*/
|
|
104194
|
+
/**
|
|
104195
|
+
* Long enough for a cold start that has migrations to apply, short enough that
|
|
104196
|
+
* a broken update is reported while the operator is still watching.
|
|
104197
|
+
*/
|
|
104198
|
+
const SERVICE_START_TIMEOUT = Duration.seconds(90);
|
|
104199
|
+
const POLL_INTERVAL = Duration.millis(500);
|
|
104200
|
+
/**
|
|
104201
|
+
* Poll until the expected build reports itself listening, the database is
|
|
104202
|
+
* marked for restore, or the timeout runs out.
|
|
104203
|
+
*
|
|
104204
|
+
* A restore marker ends the wait immediately: the migration guard writes it
|
|
104205
|
+
* only when the new build failed to migrate, and every later restart repeats
|
|
104206
|
+
* that failure, so there is nothing to wait for.
|
|
104207
|
+
*/
|
|
104208
|
+
const awaitServiceStart = Effect.fn("awaitServiceStart")(function* (input) {
|
|
104209
|
+
const deadline = DateTime.addDuration(yield* DateTime.now, input.timeout ?? SERVICE_START_TIMEOUT);
|
|
104210
|
+
while (true) {
|
|
104211
|
+
const runtimeState = yield* readPersistedServerRuntimeState(input.serverRuntimeStatePath);
|
|
104212
|
+
if (Option.isSome(runtimeState)) {
|
|
104213
|
+
const state = runtimeState.value;
|
|
104214
|
+
const startedAt = DateTime.makeUnsafe(state.startedAt);
|
|
104215
|
+
if (state.cliVersion === input.expectedVersion && DateTime.isGreaterThanOrEqualTo(startedAt, input.startedAfter)) return {
|
|
104216
|
+
_tag: "started",
|
|
104217
|
+
port: state.port
|
|
104218
|
+
};
|
|
104219
|
+
}
|
|
104220
|
+
if (yield* databaseRestorePending(input.dbPath).pipe(Effect.orElseSucceed(() => false))) return { _tag: "migration-failed" };
|
|
104221
|
+
if (DateTime.isGreaterThanOrEqualTo(yield* DateTime.now, deadline)) return { _tag: "timeout" };
|
|
104222
|
+
yield* Effect.sleep(POLL_INTERVAL);
|
|
104223
|
+
}
|
|
104224
|
+
});
|
|
104225
|
+
//#endregion
|
|
104226
|
+
//#region src/service/updateState.ts
|
|
104227
|
+
/**
|
|
104228
|
+
* ServiceUpdateState - what the last service update did, and how it ended.
|
|
104229
|
+
*
|
|
104230
|
+
* `launchctl kickstart` and `systemctl restart` both return 0 as soon as the
|
|
104231
|
+
* supervisor accepts the request, which says nothing about whether the child
|
|
104232
|
+
* actually booted. Without a record written by the update itself, a service
|
|
104233
|
+
* that dies on every start - a failed migration being the case this exists for
|
|
104234
|
+
* - looks like a successful update from the command line.
|
|
104235
|
+
*
|
|
104236
|
+
* The file is small and written atomically, so a crash mid-update leaves either
|
|
104237
|
+
* the previous contents or the new ones, never a half-written record.
|
|
104238
|
+
*
|
|
104239
|
+
* @module ServiceUpdateState
|
|
104240
|
+
*/
|
|
104241
|
+
/**
|
|
104242
|
+
* - `updating`: an update is in flight; a file left in this state means the
|
|
104243
|
+
* updating command itself died before it could record an outcome.
|
|
104244
|
+
* - `rolled-back`: the new build did not come up and the previous unit was put
|
|
104245
|
+
* back.
|
|
104246
|
+
* - `failed`: the new build did not come up and nothing could be reverted -
|
|
104247
|
+
* the case where an in-place package upgrade left no previous build on disk.
|
|
104248
|
+
*/
|
|
104249
|
+
const ServiceUpdateStatus = Schema$1.Literals([
|
|
104250
|
+
"idle",
|
|
104251
|
+
"updating",
|
|
104252
|
+
"rolled-back",
|
|
104253
|
+
"failed"
|
|
104254
|
+
]);
|
|
104255
|
+
const ServiceUpdateState = Schema$1.Struct({
|
|
104256
|
+
version: Schema$1.Literal(1),
|
|
104257
|
+
status: ServiceUpdateStatus,
|
|
104258
|
+
/** Version the service is expected to be running now. */
|
|
104259
|
+
activeVersion: Schema$1.String,
|
|
104260
|
+
/** Version an in-flight update is moving to. */
|
|
104261
|
+
pendingVersion: Schema$1.optional(Schema$1.String),
|
|
104262
|
+
recordedAt: Schema$1.String,
|
|
104263
|
+
/** Why an update ended the way it did, for `service status` to repeat. */
|
|
104264
|
+
detail: Schema$1.optional(Schema$1.String)
|
|
104265
|
+
});
|
|
104266
|
+
const SERVICE_UPDATE_STATE_FILE = "service-state.json";
|
|
104267
|
+
const serviceUpdateStatePath = Effect.fn("serviceUpdateStatePath")(function* (baseDir) {
|
|
104268
|
+
return (yield* Path.Path).join(baseDir, "runtime", SERVICE_UPDATE_STATE_FILE);
|
|
104269
|
+
});
|
|
104270
|
+
const ServiceUpdateStateJson = Schema$1.fromJsonString(ServiceUpdateState);
|
|
104271
|
+
const decodeServiceUpdateState = Schema$1.decodeUnknownEffect(ServiceUpdateStateJson);
|
|
104272
|
+
const encodeServiceUpdateState = Schema$1.encodeEffect(ServiceUpdateStateJson);
|
|
104273
|
+
/**
|
|
104274
|
+
* Read the record, or none.
|
|
104275
|
+
*
|
|
104276
|
+
* Every failure reads as none: a missing file is the normal first-update case,
|
|
104277
|
+
* and a corrupt one must not be the reason an update refuses to run.
|
|
104278
|
+
*/
|
|
104279
|
+
const readServiceUpdateState = Effect.fn("readServiceUpdateState")(function* (baseDir) {
|
|
104280
|
+
const fs = yield* FileSystem.FileSystem;
|
|
104281
|
+
const statePath = yield* serviceUpdateStatePath(baseDir);
|
|
104282
|
+
const contents = yield* fs.readFileString(statePath).pipe(Effect.option);
|
|
104283
|
+
if (Option.isNone(contents)) return Option.none();
|
|
104284
|
+
const trimmed = contents.value.trim();
|
|
104285
|
+
if (trimmed.length === 0) return Option.none();
|
|
104286
|
+
return yield* decodeServiceUpdateState(trimmed).pipe(Effect.map(Option.some), Effect.catchCause((cause) => Effect.logWarning("Ignoring an unreadable service update record").pipe(Effect.annotateLogs({
|
|
104287
|
+
statePath,
|
|
104288
|
+
cause
|
|
104289
|
+
}), Effect.as(Option.none()))));
|
|
104290
|
+
});
|
|
104291
|
+
const writeServiceUpdateState = Effect.fn("writeServiceUpdateState")(function* (input) {
|
|
104292
|
+
const statePath = yield* serviceUpdateStatePath(input.baseDir);
|
|
104293
|
+
const now = yield* DateTime.now;
|
|
104294
|
+
const state = {
|
|
104295
|
+
version: 1,
|
|
104296
|
+
status: input.status,
|
|
104297
|
+
activeVersion: input.activeVersion,
|
|
104298
|
+
...input.pendingVersion === void 0 ? {} : { pendingVersion: input.pendingVersion },
|
|
104299
|
+
recordedAt: DateTime.formatIso(now),
|
|
104300
|
+
...input.detail === void 0 ? {} : { detail: input.detail }
|
|
104301
|
+
};
|
|
104302
|
+
yield* writeFileStringAtomically({
|
|
104303
|
+
filePath: statePath,
|
|
104304
|
+
contents: `${yield* encodeServiceUpdateState(state)}\n`
|
|
104305
|
+
});
|
|
104306
|
+
return state;
|
|
104307
|
+
});
|
|
104308
|
+
/** One line for `service status`, or null when the last update went fine. */
|
|
104309
|
+
function describeServiceUpdateState(state) {
|
|
104310
|
+
switch (state.status) {
|
|
104311
|
+
case "idle": return null;
|
|
104312
|
+
case "updating": return `Last update to @p4code/cli@${state.pendingVersion ?? "unknown"} did not finish. Run \`p4c service update\` again.`;
|
|
104313
|
+
case "rolled-back": return `Last update to @p4code/cli@${state.pendingVersion ?? "unknown"} was rolled back to @p4code/cli@${state.activeVersion}${state.detail === void 0 ? "" : `: ${state.detail}`}`;
|
|
104314
|
+
case "failed": return `Last update to @p4code/cli@${state.pendingVersion ?? "unknown"} failed and could not be reverted${state.detail === void 0 ? "" : `: ${state.detail}`}`;
|
|
104315
|
+
}
|
|
104316
|
+
}
|
|
104317
|
+
//#endregion
|
|
103784
104318
|
//#region src/cli/service.ts
|
|
103785
104319
|
const isBootServiceCommandError = Schema$1.is(BootServiceCommandError);
|
|
103786
104320
|
const bootServiceLayer = (config) => layer$55({
|
|
@@ -103788,6 +104322,84 @@ const bootServiceLayer = (config) => layer$55({
|
|
|
103788
104322
|
logsDir: config.logsDir,
|
|
103789
104323
|
cliVersion: version
|
|
103790
104324
|
}).pipe(Layer.provide(layer$61));
|
|
104325
|
+
/**
|
|
104326
|
+
* The update installed a build that never reported itself listening.
|
|
104327
|
+
*
|
|
104328
|
+
* A typed failure rather than a printed warning: `p4c service update` exiting 0
|
|
104329
|
+
* after a rolled-back update is the same lie as a supervisor reporting success
|
|
104330
|
+
* for a process that died on start.
|
|
104331
|
+
*/
|
|
104332
|
+
var ServiceUpdateNotHealthyError = class extends Schema$1.TaggedErrorClass()("ServiceUpdateNotHealthyError", {
|
|
104333
|
+
attemptedVersion: Schema$1.String,
|
|
104334
|
+
previousVersion: Schema$1.String,
|
|
104335
|
+
rolledBack: Schema$1.Boolean,
|
|
104336
|
+
detail: Schema$1.String
|
|
104337
|
+
}) {
|
|
104338
|
+
get message() {
|
|
104339
|
+
return this.rolledBack ? `Update to @p4code/cli@${this.attemptedVersion} was rolled back to @p4code/cli@${this.previousVersion}: ${this.detail}.` : `Update to @p4code/cli@${this.attemptedVersion} failed and could not be reverted: ${this.detail}.`;
|
|
104340
|
+
}
|
|
104341
|
+
};
|
|
104342
|
+
/**
|
|
104343
|
+
* Update the service and then wait to see whether it came back.
|
|
104344
|
+
*
|
|
104345
|
+
* `launchctl kickstart` and `systemctl restart` report that the supervisor
|
|
104346
|
+
* accepted the request, not that the child booted - so an update that installs
|
|
104347
|
+
* a build which dies on every start used to print success while the machine
|
|
104348
|
+
* respawned a broken server every few seconds. This waits for the new build to
|
|
104349
|
+
* report itself listening and puts the previous unit back if it never does.
|
|
104350
|
+
*/
|
|
104351
|
+
const updateServiceGuarded = Effect.fn("cli.service.updateGuarded")(function* (input) {
|
|
104352
|
+
const service = yield* BootService;
|
|
104353
|
+
const status = yield* service.status;
|
|
104354
|
+
const previousUnit = yield* service.readUnit;
|
|
104355
|
+
const paths = yield* deriveServerPaths(status.baseDir, void 0);
|
|
104356
|
+
const runningState = yield* readPersistedServerRuntimeState(paths.serverRuntimeStatePath);
|
|
104357
|
+
const recordedState = yield* readServiceUpdateState(status.baseDir);
|
|
104358
|
+
const previousVersion = Option.flatMap(runningState, (state) => Option.fromNullishOr(state.cliVersion)).pipe(Option.orElse(() => Option.map(recordedState, (state) => state.activeVersion)), Option.getOrElse(() => "unknown"));
|
|
104359
|
+
const startedAfter = yield* DateTime.now;
|
|
104360
|
+
yield* writeServiceUpdateState({
|
|
104361
|
+
baseDir: status.baseDir,
|
|
104362
|
+
status: "updating",
|
|
104363
|
+
activeVersion: previousVersion,
|
|
104364
|
+
pendingVersion: input.cliVersion
|
|
104365
|
+
});
|
|
104366
|
+
const result = yield* reconcileService();
|
|
104367
|
+
if (!result.changed) yield* service.restart;
|
|
104368
|
+
const outcome = yield* awaitServiceStart({
|
|
104369
|
+
serverRuntimeStatePath: paths.serverRuntimeStatePath,
|
|
104370
|
+
dbPath: paths.dbPath,
|
|
104371
|
+
expectedVersion: input.cliVersion,
|
|
104372
|
+
startedAfter,
|
|
104373
|
+
...input.timeout === void 0 ? {} : { timeout: input.timeout }
|
|
104374
|
+
});
|
|
104375
|
+
if (outcome._tag === "started") {
|
|
104376
|
+
yield* writeServiceUpdateState({
|
|
104377
|
+
baseDir: status.baseDir,
|
|
104378
|
+
status: "idle",
|
|
104379
|
+
activeVersion: input.cliVersion
|
|
104380
|
+
});
|
|
104381
|
+
return {
|
|
104382
|
+
_tag: "started",
|
|
104383
|
+
result
|
|
104384
|
+
};
|
|
104385
|
+
}
|
|
104386
|
+
const detail = outcome._tag === "migration-failed" ? "a database migration failed, and the database was restored to the previous schema" : "the updated service did not start in time";
|
|
104387
|
+
const installedUnit = yield* service.readUnit;
|
|
104388
|
+
yield* service.revertUnit(previousUnit);
|
|
104389
|
+
const rolledBack = Option.isSome(previousUnit) && Option.getOrElse(installedUnit, () => "") !== previousUnit.value;
|
|
104390
|
+
yield* writeServiceUpdateState({
|
|
104391
|
+
baseDir: status.baseDir,
|
|
104392
|
+
status: rolledBack ? "rolled-back" : "failed",
|
|
104393
|
+
activeVersion: rolledBack ? previousVersion : input.cliVersion,
|
|
104394
|
+
pendingVersion: input.cliVersion,
|
|
104395
|
+
detail
|
|
104396
|
+
});
|
|
104397
|
+
return {
|
|
104398
|
+
_tag: rolledBack ? "rolled-back" : "failed",
|
|
104399
|
+
detail,
|
|
104400
|
+
previousVersion
|
|
104401
|
+
};
|
|
104402
|
+
});
|
|
103791
104403
|
/** Install, update, or repair the service using the CLI version running this command. */
|
|
103792
104404
|
const reconcileService = Effect.fn("cli.service.reconcile")(function* () {
|
|
103793
104405
|
const service = yield* BootService;
|
|
@@ -103817,7 +104429,7 @@ function formatReconcileSuccess(input) {
|
|
|
103817
104429
|
...input.platform === "darwin" && !input.previouslyInstalled ? [LAUNCH_AGENT_LOGIN_NOTE] : []
|
|
103818
104430
|
].join("\n");
|
|
103819
104431
|
}
|
|
103820
|
-
function formatServiceStatus(status, cliVersion) {
|
|
104432
|
+
function formatServiceStatus(status, cliVersion, lastUpdate) {
|
|
103821
104433
|
if (!status.supported) return "P4Code service\n Status: unavailable on this machine\n Supported on: Linux with systemd, macOS with launchd";
|
|
103822
104434
|
if (!status.installed) return "P4Code service\n Status: not installed\n Next: Run `p4c service install`.";
|
|
103823
104435
|
return [
|
|
@@ -103826,6 +104438,7 @@ function formatServiceStatus(status, cliVersion) {
|
|
|
103826
104438
|
` Unit: ${status.unitPath}`,
|
|
103827
104439
|
` Data: ${status.baseDir}`,
|
|
103828
104440
|
` Logs: ${status.logPath}`,
|
|
104441
|
+
...lastUpdate ? [` Last update: ${lastUpdate}`] : [],
|
|
103829
104442
|
...status.current ? [] : [" Next: Run `p4c service update`."]
|
|
103830
104443
|
].join("\n");
|
|
103831
104444
|
}
|
|
@@ -103891,15 +104504,24 @@ const serviceInstallCommand = Command.make("install", {
|
|
|
103891
104504
|
tailscaleServePort: flags.tailscaleServePort
|
|
103892
104505
|
}))));
|
|
103893
104506
|
const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe(Command.withDescription("Update or repair the background service so it runs this CLI build, then restart it."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
|
|
103894
|
-
const
|
|
103895
|
-
if (
|
|
103896
|
-
|
|
104507
|
+
const outcome = yield* updateServiceGuarded({ cliVersion: version });
|
|
104508
|
+
if (outcome._tag !== "started") {
|
|
104509
|
+
const service = yield* BootService;
|
|
104510
|
+
yield* Console.error(`Logs: ${service.logPath}`);
|
|
104511
|
+
return yield* new ServiceUpdateNotHealthyError({
|
|
104512
|
+
attemptedVersion: version,
|
|
104513
|
+
previousVersion: outcome.previousVersion,
|
|
104514
|
+
rolledBack: outcome._tag === "rolled-back",
|
|
104515
|
+
detail: outcome.detail
|
|
104516
|
+
});
|
|
104517
|
+
}
|
|
104518
|
+
if (!outcome.result.changed) {
|
|
103897
104519
|
yield* Console.log(`Restarted the P4Code service on @p4code/cli@${version}.`);
|
|
103898
104520
|
return;
|
|
103899
104521
|
}
|
|
103900
104522
|
yield* Console.log(formatReconcileSuccess({
|
|
103901
|
-
previouslyInstalled: result.previouslyInstalled,
|
|
103902
|
-
plan: result.plan,
|
|
104523
|
+
previouslyInstalled: outcome.result.previouslyInstalled,
|
|
104524
|
+
plan: outcome.result.plan,
|
|
103903
104525
|
cliVersion: version,
|
|
103904
104526
|
platform: yield* HostProcessPlatform
|
|
103905
104527
|
}));
|
|
@@ -103960,8 +104582,12 @@ const serviceSelfUpdateCommand = Command.make("self-update", {
|
|
|
103960
104582
|
yield* Console.log(`Service now runs @p4code/cli@${installed}.`);
|
|
103961
104583
|
}))));
|
|
103962
104584
|
const serviceStatusCommand = Command.make("status", projectLocationFlags).pipe(Command.withDescription("Show whether the P4Code background service is installed."), Command.withHandler((flags) => runServiceCommand(flags, Effect.gen(function* () {
|
|
103963
|
-
const
|
|
103964
|
-
yield*
|
|
104585
|
+
const status = yield* (yield* BootService).status;
|
|
104586
|
+
const recorded = yield* readServiceUpdateState(status.baseDir);
|
|
104587
|
+
yield* Console.log(formatServiceStatus(status, version, Option.match(recorded, {
|
|
104588
|
+
onNone: () => null,
|
|
104589
|
+
onSome: describeServiceUpdateState
|
|
104590
|
+
})));
|
|
103965
104591
|
}))));
|
|
103966
104592
|
Effect.gen(function* () {
|
|
103967
104593
|
const { supported, installed, current } = yield* (yield* BootService).status;
|