@p4code/cli 0.1.7 → 0.1.9
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 +517 -202
- package/dist/client/assets/{DiffPanel-B_RUbYdJ.js → DiffPanel-Bb72saoS.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-D_TE6anL.js → FilePreviewPanel-6mJ-Vsub.js} +2 -2
- package/dist/client/assets/{PreviewPanel-Dx5mB3w6.js → PreviewPanel-ogPpU8Ev.js} +2 -2
- package/dist/client/assets/{angular-html-Z6GyEAg_.js → angular-html-BIzUKsg7.js} +2 -2
- package/dist/client/assets/{angular-ts-BdawRcND.js → angular-ts-CSIA-4Zf.js} +2 -2
- package/dist/client/assets/{apl-0Nzwg3c9.js → apl-Du1uuPDy.js} +2 -2
- package/dist/client/assets/arrow-right-qNd2lFlZ.js +2 -0
- package/dist/client/assets/{astro-iJpYXO_m.js → astro-bVpCfC8I.js} +2 -2
- package/dist/client/assets/{blade-BeMtIYzp.js → blade-KOd_hdA5.js} +2 -2
- package/dist/client/assets/{c-n5Rj77uj.js → c-BUOUjYKH.js} +2 -2
- package/dist/client/assets/{cobol-CfsSyjzP.js → cobol-DVr6VPEj.js} +2 -2
- package/dist/client/assets/{coffee-LZB1mvnz.js → coffee-CaWTiLWu.js} +2 -2
- package/dist/client/assets/{cpp-C689OQY_.js → cpp-DOVxTtfq.js} +2 -2
- package/dist/client/assets/{crystal-CC7u-hEs.js → crystal-D_Q817Qu.js} +2 -2
- package/dist/client/assets/{css-eVqGM3Yx.js → css-Rez0Ry86.js} +2 -2
- package/dist/client/assets/{edge-CvU3NO69.js → edge-C5CKZJyo.js} +2 -2
- package/dist/client/assets/{elixir-gblyDHJ2.js → elixir-D_eVXxgH.js} +2 -2
- package/dist/client/assets/{elm-BEDREZ-r.js → elm-BLkMkUJ0.js} +2 -2
- package/dist/client/assets/{erb-C_pVTICR.js → erb-DGG0CxNR.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-Z0wcn9Z2.js → fileCommentAnnotations-D0gz9Nvl.js} +2 -2
- package/dist/client/assets/{git-rebase-BvBzTqE6.js → git-rebase-D7H5ArnN.js} +2 -2
- package/dist/client/assets/{glimmer-js-y6xvI0dn.js → glimmer-js-DOuFsyYy.js} +2 -2
- package/dist/client/assets/{glimmer-ts-BrWleVTO.js → glimmer-ts-C_dgMrVH.js} +2 -2
- package/dist/client/assets/{glsl-Bkrdoinn.js → glsl-BJU0wvLz.js} +2 -2
- package/dist/client/assets/{graphql-CSx1KLSt.js → graphql-CXUg7dVm.js} +2 -2
- package/dist/client/assets/{hack-CEPENt6B.js → hack-DEwTpBBH.js} +2 -2
- package/dist/client/assets/{haml-CR4MMWD-.js → haml-Br-Q1jfj.js} +2 -2
- package/dist/client/assets/{handlebars-BvZ81eM2.js → handlebars-U-qCMjLv.js} +2 -2
- package/dist/client/assets/{html-CJJ_Yk-p.js → html-CBhkruiu.js} +2 -2
- package/dist/client/assets/{html-derivative-DyGTUpT_.js → html-derivative-CtOWonuc.js} +2 -2
- package/dist/client/assets/{http-CthnayZT.js → http-B-anb7K2.js} +2 -2
- package/dist/client/assets/{hurl-CySpRHwn.js → hurl-UbSvk6zv.js} +2 -2
- package/dist/client/assets/{index-CTDRPB7h.js → index-CADZUNMb.js} +203 -203
- package/dist/client/assets/index-CPnR_Tju.css +1 -0
- package/dist/client/assets/{java-eLetcmnB.js → java-d4Ae9Kq8.js} +2 -2
- package/dist/client/assets/{javascript-bze-DcnN.js → javascript-DENutVTd.js} +2 -2
- package/dist/client/assets/{jinja-BwBfXVzv.js → jinja-Bugh_6jh.js} +2 -2
- package/dist/client/assets/{jison-BG-6HBeN.js → jison-5v-Z1oHG.js} +2 -2
- package/dist/client/assets/{json-D04kql_u.js → json-BhFmIQh2.js} +2 -2
- package/dist/client/assets/{jsx-B3RhaC78.js → jsx-ByAWu7n0.js} +2 -2
- package/dist/client/assets/{julia-CLYU2NBT.js → julia-DE4JKJ_I.js} +2 -2
- package/dist/client/assets/{just-CQGSktLF.js → just-Ck-bQzBW.js} +2 -2
- package/dist/client/assets/{latex-B2r18a0i.js → latex-0aopGn_u.js} +2 -2
- package/dist/client/assets/{liquid-DSPVRc0S.js → liquid-BOV6YY-4.js} +2 -2
- package/dist/client/assets/{lua-CBxs-my9.js → lua-BS79SG5v.js} +2 -2
- package/dist/client/assets/{marko-FkXUABHT.js → marko-CxCHVwvS.js} +2 -2
- package/dist/client/assets/{mdc-BW-qJ07Y.js → mdc-D8ArMX_3.js} +2 -2
- package/dist/client/assets/{nginx-r-F4ISTc.js → nginx-54lPKP4l.js} +2 -2
- package/dist/client/assets/{nim-CqGONDki.js → nim-CQW1n-W6.js} +2 -2
- package/dist/client/assets/{perl-YqxSDkbB.js → perl-TGEf9BSy.js} +2 -2
- package/dist/client/assets/{php-o53m7ISl.js → php-CRSs0ljw.js} +2 -2
- package/dist/client/assets/{pug-DvC1IxmZ.js → pug-B94qB-T3.js} +2 -2
- package/dist/client/assets/{qml-B_UHgwrf.js → qml-Ce-jpaqY.js} +2 -2
- package/dist/client/assets/{r-BslRtXqA.js → r-Di-hq60i.js} +2 -2
- package/dist/client/assets/{razor-DbFRY8EJ.js → razor-VXC_c_VY.js} +2 -2
- package/dist/client/assets/{regexp-LUjvkiV5.js → regexp-DCuWLst8.js} +2 -2
- package/dist/client/assets/{rst-DjMRC4dl.js → rst-CaleKCxt.js} +2 -2
- package/dist/client/assets/{ruby-D1SSXqCW.js → ruby-BNvjj0Xx.js} +2 -2
- package/dist/client/assets/{sas-8pH9ZpYC.js → sas-CN3Jyhxd.js} +2 -2
- package/dist/client/assets/{scss-CLXZYmiL.js → scss-iGZoIRyN.js} +2 -2
- package/dist/client/assets/{shellscript-C3x4iGUN.js → shellscript-B3Nk58br.js} +2 -2
- package/dist/client/assets/{shellsession-4iw-70AB.js → shellsession-aV0EL5Me.js} +2 -2
- package/dist/client/assets/{soy-BmuiXqhy.js → soy-CK-MnjGK.js} +2 -2
- package/dist/client/assets/{sql-CYtr1JGt.js → sql-WiqY-pg6.js} +2 -2
- package/dist/client/assets/{stata-D-p-ifu4.js → stata-L2KCRIsj.js} +2 -2
- package/dist/client/assets/{surrealql-BQtaSVD9.js → surrealql-y8rFKpHp.js} +2 -2
- package/dist/client/assets/{svelte-BZ0IuCtg.js → svelte-BUjzGJjC.js} +2 -2
- package/dist/client/assets/{templ-D_0C9uOG.js → templ-Lwa6_2Pi.js} +2 -2
- package/dist/client/assets/{tex-Da564oMl.js → tex-C6_2o8N6.js} +2 -2
- package/dist/client/assets/textarea-C26NLrE0.js +46 -0
- package/dist/client/assets/{ts-tags-CCvt3g0S.js → ts-tags-DHGXrdlY.js} +2 -2
- package/dist/client/assets/{tsx-C1pKkLrg.js → tsx-D7a2s_rX.js} +2 -2
- package/dist/client/assets/{twig-ulf4RPD9.js → twig-rodlH6R_.js} +2 -2
- package/dist/client/assets/{typescript-Cm2BDYNJ.js → typescript-BSVnlc3E.js} +2 -2
- package/dist/client/assets/{vue-D38YVq_o.js → vue-CPqhBG4f.js} +2 -2
- package/dist/client/assets/{vue-html-Dw9Iwm-j.js → vue-html-DlcYUp9j.js} +2 -2
- package/dist/client/assets/{vue-vine-DGhLcVZx.js → vue-vine-C33vB5eU.js} +2 -2
- package/dist/client/assets/{xml-Bm9IDqQ-.js → xml-Dk7HbObH.js} +2 -2
- package/dist/client/assets/{xsl-CLGFKBCX.js → xsl-Ce_0ufBe.js} +2 -2
- package/dist/client/assets/{yaml-C5Olrwb1.js → yaml-hWCzaJrB.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/arrow-right-BVDRuCFE.js +0 -2
- package/dist/client/assets/index-CstNfQU9.css +0 -1
- package/dist/client/assets/textarea-CAyleL6s.js +0 -46
package/dist/bin.mjs
CHANGED
|
@@ -233,7 +233,7 @@ const make$73 = () => {
|
|
|
233
233
|
const layer$69 = Layer.sync(NetService, make$73);
|
|
234
234
|
//#endregion
|
|
235
235
|
//#region package.json
|
|
236
|
-
var version = "0.1.
|
|
236
|
+
var version = "0.1.9";
|
|
237
237
|
//#endregion
|
|
238
238
|
//#region src/config.ts
|
|
239
239
|
/**
|
|
@@ -7012,175 +7012,6 @@ const GitActionProgressEvent = Schema$1.Union([
|
|
|
7012
7012
|
GitActionFailedEvent
|
|
7013
7013
|
]);
|
|
7014
7014
|
//#endregion
|
|
7015
|
-
//#region ../../packages/contracts/src/p4ProjectFile.ts
|
|
7016
|
-
/** File name of the checked-in P4 project file, resolved at the workspace root. */
|
|
7017
|
-
const P4_PROJECT_FILE_NAME = "p4.json";
|
|
7018
|
-
/** Public URL of the published JSON Schema for {@link P4ProjectFile}. */
|
|
7019
|
-
const P4_PROJECT_FILE_SCHEMA_URL = "https://p4.codes/schema/p4.json";
|
|
7020
|
-
const P4_PROJECT_FILE_PATH_MAX_LENGTH = 512;
|
|
7021
|
-
const P4_PROJECT_FILE_MAX_SCRIPTS = 50;
|
|
7022
|
-
const trimmedNonEmpty = (annotations, maxLength) => {
|
|
7023
|
-
const annotated = Schema$1.String.annotate(annotations);
|
|
7024
|
-
const encoded = maxLength === void 0 ? annotated.check(Schema$1.isNonEmpty()) : annotated.check(Schema$1.isNonEmpty(), Schema$1.isMaxLength(maxLength));
|
|
7025
|
-
return encoded.pipe(Schema$1.decodeTo(encoded, SchemaTransformation.trim()));
|
|
7026
|
-
};
|
|
7027
|
-
const P4ProjectFileScript = Schema$1.Struct({
|
|
7028
|
-
name: trimmedNonEmpty({ description: "Display name for the script, shown in the P4Code scripts menu." }),
|
|
7029
|
-
command: trimmedNonEmpty({ description: "Shell command executed in a P4Code terminal at the project root." }),
|
|
7030
|
-
icon: Schema$1.optionalKey(ProjectScriptIcon.annotate({ description: "Icon shown next to the script in the scripts menu. Defaults to \"play\"." })),
|
|
7031
|
-
runOnWorktreeCreate: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, the script runs automatically after a worktree is created for a new thread." })),
|
|
7032
|
-
previewUrl: Schema$1.optionalKey(trimmedNonEmpty({ description: "URL opened in the in-app browser preview when this script runs. Only honored on the desktop build." })),
|
|
7033
|
-
autoOpenPreview: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, automatically open the preview panel at `previewUrl` the moment the script starts." }))
|
|
7034
|
-
}).annotate({ description: "A project script that team members can import into P4Code." });
|
|
7035
|
-
const P4ProjectFile = Schema$1.Struct({
|
|
7036
|
-
$schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
|
|
7037
|
-
iconPath: Schema$1.optionalKey(trimmedNonEmpty({ description: "Workspace-relative path to the project icon (e.g. \"assets/logo.svg\"). Checked before P4Code's built-in icon locations." }, P4_PROJECT_FILE_PATH_MAX_LENGTH)),
|
|
7038
|
-
scripts: Schema$1.optionalKey(Schema$1.Array(P4ProjectFileScript).annotate({ description: "Project scripts shared with everyone who opens this repository in P4Code." }).check(Schema$1.isMaxLength(P4_PROJECT_FILE_MAX_SCRIPTS)))
|
|
7039
|
-
}).annotate({
|
|
7040
|
-
title: "P4 project file",
|
|
7041
|
-
description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
|
|
7042
|
-
});
|
|
7043
|
-
//#endregion
|
|
7044
|
-
//#region ../../packages/contracts/src/project.ts
|
|
7045
|
-
const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
|
|
7046
|
-
const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
|
|
7047
|
-
const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512;
|
|
7048
|
-
const ProjectSearchEntriesInput = Schema$1.Struct({
|
|
7049
|
-
cwd: TrimmedNonEmptyString,
|
|
7050
|
-
query: TrimmedNonEmptyString.check(Schema$1.isMaxLength(256)),
|
|
7051
|
-
limit: PositiveInt.check(Schema$1.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT))
|
|
7052
|
-
});
|
|
7053
|
-
const ProjectEntryKind = Schema$1.Literals(["file", "directory"]);
|
|
7054
|
-
const ProjectEntry = Schema$1.Struct({
|
|
7055
|
-
path: TrimmedNonEmptyString,
|
|
7056
|
-
kind: ProjectEntryKind
|
|
7057
|
-
});
|
|
7058
|
-
const ProjectSearchEntriesResult = Schema$1.Struct({
|
|
7059
|
-
entries: Schema$1.Array(ProjectEntry),
|
|
7060
|
-
truncated: Schema$1.Boolean
|
|
7061
|
-
});
|
|
7062
|
-
const ProjectListEntriesInput = Schema$1.Struct({ cwd: TrimmedNonEmptyString });
|
|
7063
|
-
const ProjectListEntriesResult = Schema$1.Struct({
|
|
7064
|
-
entries: Schema$1.Array(ProjectEntry),
|
|
7065
|
-
truncated: Schema$1.Boolean
|
|
7066
|
-
});
|
|
7067
|
-
const ProjectEntriesFailure = Schema$1.Literals([
|
|
7068
|
-
"workspace_root_not_found",
|
|
7069
|
-
"workspace_root_create_failed",
|
|
7070
|
-
"workspace_root_stat_failed",
|
|
7071
|
-
"workspace_root_not_directory",
|
|
7072
|
-
"search_index_create_failed",
|
|
7073
|
-
"search_index_scan_timed_out",
|
|
7074
|
-
"search_index_search_failed"
|
|
7075
|
-
]);
|
|
7076
|
-
function decodedProjectErrorMessage(props) {
|
|
7077
|
-
if (!("message" in props)) return void 0;
|
|
7078
|
-
return typeof props.message === "string" ? props.message : void 0;
|
|
7079
|
-
}
|
|
7080
|
-
var ProjectSearchEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectSearchEntriesError", {
|
|
7081
|
-
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7082
|
-
queryLength: Schema$1.optional(NonNegativeInt),
|
|
7083
|
-
limit: Schema$1.optional(PositiveInt),
|
|
7084
|
-
failure: Schema$1.optional(ProjectEntriesFailure),
|
|
7085
|
-
normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7086
|
-
timeout: Schema$1.optional(TrimmedNonEmptyString),
|
|
7087
|
-
detail: Schema$1.optional(TrimmedNonEmptyString),
|
|
7088
|
-
message: TrimmedNonEmptyString,
|
|
7089
|
-
cause: Schema$1.optional(Schema$1.Defect())
|
|
7090
|
-
}) {
|
|
7091
|
-
constructor(props) {
|
|
7092
|
-
super({
|
|
7093
|
-
...props,
|
|
7094
|
-
message: decodedProjectErrorMessage(props) ?? `Failed to search workspace entries in '${props.cwd}'.`
|
|
7095
|
-
});
|
|
7096
|
-
}
|
|
7097
|
-
};
|
|
7098
|
-
var ProjectListEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectListEntriesError", {
|
|
7099
|
-
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7100
|
-
failure: Schema$1.optional(ProjectEntriesFailure),
|
|
7101
|
-
normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7102
|
-
timeout: Schema$1.optional(TrimmedNonEmptyString),
|
|
7103
|
-
detail: Schema$1.optional(TrimmedNonEmptyString),
|
|
7104
|
-
message: TrimmedNonEmptyString,
|
|
7105
|
-
cause: Schema$1.optional(Schema$1.Defect())
|
|
7106
|
-
}) {
|
|
7107
|
-
constructor(props) {
|
|
7108
|
-
super({
|
|
7109
|
-
...props,
|
|
7110
|
-
message: decodedProjectErrorMessage(props) ?? `Failed to list workspace entries in '${props.cwd}'.`
|
|
7111
|
-
});
|
|
7112
|
-
}
|
|
7113
|
-
};
|
|
7114
|
-
const ProjectReadFileInput = Schema$1.Struct({
|
|
7115
|
-
cwd: TrimmedNonEmptyString,
|
|
7116
|
-
relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_READ_FILE_PATH_MAX_LENGTH))
|
|
7117
|
-
});
|
|
7118
|
-
const ProjectReadFileResult = Schema$1.Struct({
|
|
7119
|
-
relativePath: TrimmedNonEmptyString,
|
|
7120
|
-
contents: Schema$1.String,
|
|
7121
|
-
byteLength: NonNegativeInt,
|
|
7122
|
-
truncated: Schema$1.Boolean
|
|
7123
|
-
});
|
|
7124
|
-
const ProjectFileFailure = Schema$1.Literals([
|
|
7125
|
-
"workspace_path_outside_root",
|
|
7126
|
-
"resolved_path_outside_root",
|
|
7127
|
-
"path_not_file",
|
|
7128
|
-
"binary_file",
|
|
7129
|
-
"operation_failed"
|
|
7130
|
-
]);
|
|
7131
|
-
const ProjectFileOperation = Schema$1.Literals([
|
|
7132
|
-
"realpath-workspace-root",
|
|
7133
|
-
"realpath-target",
|
|
7134
|
-
"open",
|
|
7135
|
-
"stat",
|
|
7136
|
-
"read",
|
|
7137
|
-
"close",
|
|
7138
|
-
"make-directory",
|
|
7139
|
-
"write-file"
|
|
7140
|
-
]);
|
|
7141
|
-
var ProjectReadFileError = class extends Schema$1.TaggedErrorClass()("ProjectReadFileError", {
|
|
7142
|
-
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7143
|
-
relativePath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7144
|
-
failure: Schema$1.optional(ProjectFileFailure),
|
|
7145
|
-
resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7146
|
-
resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
|
|
7147
|
-
operation: Schema$1.optional(ProjectFileOperation),
|
|
7148
|
-
operationPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7149
|
-
message: TrimmedNonEmptyString,
|
|
7150
|
-
cause: Schema$1.optional(Schema$1.Defect())
|
|
7151
|
-
}) {
|
|
7152
|
-
constructor(props) {
|
|
7153
|
-
super({
|
|
7154
|
-
...props,
|
|
7155
|
-
message: decodedProjectErrorMessage(props) ?? `Failed to read workspace file '${props.relativePath}' in '${props.cwd}'.`
|
|
7156
|
-
});
|
|
7157
|
-
}
|
|
7158
|
-
};
|
|
7159
|
-
const ProjectWriteFileInput = Schema$1.Struct({
|
|
7160
|
-
cwd: TrimmedNonEmptyString,
|
|
7161
|
-
relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)),
|
|
7162
|
-
contents: Schema$1.String
|
|
7163
|
-
});
|
|
7164
|
-
const ProjectWriteFileResult = Schema$1.Struct({ relativePath: TrimmedNonEmptyString });
|
|
7165
|
-
var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWriteFileError", {
|
|
7166
|
-
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7167
|
-
relativePath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7168
|
-
failure: Schema$1.optional(ProjectFileFailure),
|
|
7169
|
-
resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7170
|
-
resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
|
|
7171
|
-
operation: Schema$1.optional(ProjectFileOperation),
|
|
7172
|
-
operationPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7173
|
-
message: TrimmedNonEmptyString,
|
|
7174
|
-
cause: Schema$1.optional(Schema$1.Defect())
|
|
7175
|
-
}) {
|
|
7176
|
-
constructor(props) {
|
|
7177
|
-
super({
|
|
7178
|
-
...props,
|
|
7179
|
-
message: decodedProjectErrorMessage(props) ?? `Failed to write workspace file '${props.relativePath}' in '${props.cwd}'.`
|
|
7180
|
-
});
|
|
7181
|
-
}
|
|
7182
|
-
};
|
|
7183
|
-
//#endregion
|
|
7184
7015
|
//#region ../../packages/contracts/src/task.ts
|
|
7185
7016
|
const TASK_TITLE_MAX_LENGTH = 512;
|
|
7186
7017
|
const TASK_LABEL_MAX_LENGTH = 64;
|
|
@@ -7205,6 +7036,19 @@ const TaskPriority = Schema$1.Literals([
|
|
|
7205
7036
|
"high",
|
|
7206
7037
|
"urgent"
|
|
7207
7038
|
]);
|
|
7039
|
+
/**
|
|
7040
|
+
* A per-prefix sequence prefix, e.g. `T` or `P4`. Declared by a project in its
|
|
7041
|
+
* `p4.json` (`taskPrefix`); always stored and compared uppercase.
|
|
7042
|
+
*/
|
|
7043
|
+
const TASK_READABLE_ID_PREFIX_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}$/;
|
|
7044
|
+
/**
|
|
7045
|
+
* The shape of a readable task id: prefix, dash, 1-based sequence number.
|
|
7046
|
+
* Deliberately disjoint from the UUID `taskId`, which is what lets a lookup
|
|
7047
|
+
* accept either and tell them apart by shape alone.
|
|
7048
|
+
*/
|
|
7049
|
+
const TASK_READABLE_ID_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,9}-[1-9][0-9]*$/;
|
|
7050
|
+
/** Whether a task reference is a readable id (`P4-12`) rather than a UUID. */
|
|
7051
|
+
const isReadableTaskId = (reference) => TASK_READABLE_ID_PATTERN.test(reference);
|
|
7208
7052
|
const TaskLabel = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_LABEL_MAX_LENGTH));
|
|
7209
7053
|
/**
|
|
7210
7054
|
* Agent-facing field descriptions. These ride on the *encoded* side because
|
|
@@ -7232,11 +7076,37 @@ const AssigneeField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSI
|
|
|
7232
7076
|
* no remote, where there is nothing stable to key on.
|
|
7233
7077
|
*/
|
|
7234
7078
|
const RepositoryKeyField = TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Canonical repository key (normalized git remote, e.g. github.com/owner/repo). Portable across machines, unlike projectId." }));
|
|
7079
|
+
/**
|
|
7080
|
+
* Manual board position within a column, as a lexicographic fractional key.
|
|
7081
|
+
*
|
|
7082
|
+
* Compared as a plain string: smaller sorts higher. Keys are minted by the
|
|
7083
|
+
* board between the neighbours of a drop, so moving one card writes one row
|
|
7084
|
+
* rather than renumbering the column. `null` means the task has never been
|
|
7085
|
+
* placed by hand and falls back to the priority/recency order.
|
|
7086
|
+
*/
|
|
7087
|
+
const OrderKeyField = TrimmedNonEmptyString.pipe(Schema$1.annotateEncoded({ description: "Manual board position key. Lexicographic: smaller sorts higher in the column. Null means no manual position." }));
|
|
7235
7088
|
const LabelsField = Schema$1.Array(TaskLabel).check(Schema$1.isMaxLength(TASK_LABELS_MAX_COUNT)).pipe(Schema$1.annotateEncoded({ description: "Free-form tags used to group and filter tasks." }));
|
|
7236
|
-
const TaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "Id of the task to act on." }));
|
|
7089
|
+
const TaskIdField = TaskId.pipe(Schema$1.annotateEncoded({ description: "Id of the task to act on - the UUID or the readable id (e.g. P4-12)." }));
|
|
7237
7090
|
const Task = Schema$1.Struct({
|
|
7238
7091
|
taskId: TaskId,
|
|
7239
7092
|
/**
|
|
7093
|
+
* The human-readable id, e.g. `T-1` or `P4-12`: a per-prefix sequence
|
|
7094
|
+
* allocated by the store that owns the row, never reused, with gaps where
|
|
7095
|
+
* tasks were deleted. Nullable only for version skew - a row read from a
|
|
7096
|
+
* store that has not run the backfill migration decodes to `null` rather
|
|
7097
|
+
* than failing the whole board.
|
|
7098
|
+
*/
|
|
7099
|
+
readableId: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
|
|
7100
|
+
/**
|
|
7101
|
+
* Manual board position: a lexicographic fractional key, compared as a plain
|
|
7102
|
+
* string, smaller first. Written only when a card is dragged to a position;
|
|
7103
|
+
* `null` means "never placed by hand" and the board falls back to its
|
|
7104
|
+
* priority/recency order. Nullable-with-default for the same version-skew
|
|
7105
|
+
* reason as `readableId`: a row from a store without the migration decodes to
|
|
7106
|
+
* `null` rather than failing the whole board.
|
|
7107
|
+
*/
|
|
7108
|
+
orderKey: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
|
|
7109
|
+
/**
|
|
7240
7110
|
* The project this task belongs to, or `null` for a task that is not scoped
|
|
7241
7111
|
* to one. This references the existing workspace project registry rather than
|
|
7242
7112
|
* a task-board-private notion of a project.
|
|
@@ -7277,6 +7147,27 @@ const TaskCreateInput = Schema$1.Struct({
|
|
|
7277
7147
|
labels: Schema$1.optional(LabelsField)
|
|
7278
7148
|
});
|
|
7279
7149
|
/**
|
|
7150
|
+
* What an environment server sends the hub to file a task.
|
|
7151
|
+
*
|
|
7152
|
+
* Extends the public create input with the readable-id prefix, because the two
|
|
7153
|
+
* halves of the answer live on different machines: the prefix comes from the
|
|
7154
|
+
* project's checked-in `p4.json`, which only the submitting server can read,
|
|
7155
|
+
* while the sequence number must be allocated by the hub, which is the only
|
|
7156
|
+
* party that sees every machine's writes. Deliberately not part of
|
|
7157
|
+
* `TaskCreateInput` itself so an agent filing over MCP cannot pick a prefix -
|
|
7158
|
+
* the prefix belongs to the project, not to the caller.
|
|
7159
|
+
*/
|
|
7160
|
+
const HubTaskCreateInput = Schema$1.Struct({
|
|
7161
|
+
...TaskCreateInput.fields,
|
|
7162
|
+
readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString),
|
|
7163
|
+
/**
|
|
7164
|
+
* Carried on the hub's create route (but not the public one) so a server
|
|
7165
|
+
* mirroring an already-placed row to the hub does not lose its position.
|
|
7166
|
+
* An agent filing a new task has no business picking a slot.
|
|
7167
|
+
*/
|
|
7168
|
+
orderKey: Schema$1.optional(OrderKeyField)
|
|
7169
|
+
});
|
|
7170
|
+
/**
|
|
7280
7171
|
* Every field is optional and an omitted field is left untouched. The nullable
|
|
7281
7172
|
* fields accept an explicit `null` to clear them, which is why they are
|
|
7282
7173
|
* `NullOr` inside `optional` rather than merely optional.
|
|
@@ -7291,7 +7182,8 @@ const TaskUpdateInput = Schema$1.Struct({
|
|
|
7291
7182
|
status: Schema$1.optional(StatusField),
|
|
7292
7183
|
priority: Schema$1.optional(PriorityField),
|
|
7293
7184
|
assignee: Schema$1.optional(Schema$1.NullOr(AssigneeField)),
|
|
7294
|
-
labels: Schema$1.optional(LabelsField)
|
|
7185
|
+
labels: Schema$1.optional(LabelsField),
|
|
7186
|
+
orderKey: Schema$1.optional(Schema$1.NullOr(OrderKeyField))
|
|
7295
7187
|
});
|
|
7296
7188
|
/**
|
|
7297
7189
|
* A task an agent has drafted but has not filed.
|
|
@@ -7480,6 +7372,176 @@ const TaskToolError = Schema$1.Union([
|
|
|
7480
7372
|
TaskStoreError
|
|
7481
7373
|
]);
|
|
7482
7374
|
//#endregion
|
|
7375
|
+
//#region ../../packages/contracts/src/p4ProjectFile.ts
|
|
7376
|
+
/** File name of the checked-in P4 project file, resolved at the workspace root. */
|
|
7377
|
+
const P4_PROJECT_FILE_NAME = "p4.json";
|
|
7378
|
+
/** Public URL of the published JSON Schema for {@link P4ProjectFile}. */
|
|
7379
|
+
const P4_PROJECT_FILE_SCHEMA_URL = "https://p4.codes/schema/p4.json";
|
|
7380
|
+
const P4_PROJECT_FILE_PATH_MAX_LENGTH = 512;
|
|
7381
|
+
const P4_PROJECT_FILE_MAX_SCRIPTS = 50;
|
|
7382
|
+
const trimmedNonEmpty = (annotations, maxLength) => {
|
|
7383
|
+
const annotated = Schema$1.String.annotate(annotations);
|
|
7384
|
+
const encoded = maxLength === void 0 ? annotated.check(Schema$1.isNonEmpty()) : annotated.check(Schema$1.isNonEmpty(), Schema$1.isMaxLength(maxLength));
|
|
7385
|
+
return encoded.pipe(Schema$1.decodeTo(encoded, SchemaTransformation.trim()));
|
|
7386
|
+
};
|
|
7387
|
+
const P4ProjectFileScript = Schema$1.Struct({
|
|
7388
|
+
name: trimmedNonEmpty({ description: "Display name for the script, shown in the P4Code scripts menu." }),
|
|
7389
|
+
command: trimmedNonEmpty({ description: "Shell command executed in a P4Code terminal at the project root." }),
|
|
7390
|
+
icon: Schema$1.optionalKey(ProjectScriptIcon.annotate({ description: "Icon shown next to the script in the scripts menu. Defaults to \"play\"." })),
|
|
7391
|
+
runOnWorktreeCreate: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, the script runs automatically after a worktree is created for a new thread." })),
|
|
7392
|
+
previewUrl: Schema$1.optionalKey(trimmedNonEmpty({ description: "URL opened in the in-app browser preview when this script runs. Only honored on the desktop build." })),
|
|
7393
|
+
autoOpenPreview: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, automatically open the preview panel at `previewUrl` the moment the script starts." }))
|
|
7394
|
+
}).annotate({ description: "A project script that team members can import into P4Code." });
|
|
7395
|
+
const P4ProjectFile = Schema$1.Struct({
|
|
7396
|
+
$schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
|
|
7397
|
+
iconPath: Schema$1.optionalKey(trimmedNonEmpty({ description: "Workspace-relative path to the project icon (e.g. \"assets/logo.svg\"). Checked before P4Code's built-in icon locations." }, P4_PROJECT_FILE_PATH_MAX_LENGTH)),
|
|
7398
|
+
scripts: Schema$1.optionalKey(Schema$1.Array(P4ProjectFileScript).annotate({ description: "Project scripts shared with everyone who opens this repository in P4Code." }).check(Schema$1.isMaxLength(P4_PROJECT_FILE_MAX_SCRIPTS))),
|
|
7399
|
+
taskPrefix: Schema$1.optionalKey(trimmedNonEmpty({ description: "Prefix for this project's readable task ids on the task board (e.g. \"P4\" numbers tasks P4-1, P4-2, ...). Letters and digits, starting with a letter, at most 10 characters; compared case-insensitively. Tasks without a project use \"T\"." }).check(Schema$1.isPattern(TASK_READABLE_ID_PREFIX_PATTERN)))
|
|
7400
|
+
}).annotate({
|
|
7401
|
+
title: "P4 project file",
|
|
7402
|
+
description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
|
|
7403
|
+
});
|
|
7404
|
+
//#endregion
|
|
7405
|
+
//#region ../../packages/contracts/src/project.ts
|
|
7406
|
+
const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
|
|
7407
|
+
const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
|
|
7408
|
+
const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512;
|
|
7409
|
+
const ProjectSearchEntriesInput = Schema$1.Struct({
|
|
7410
|
+
cwd: TrimmedNonEmptyString,
|
|
7411
|
+
query: TrimmedNonEmptyString.check(Schema$1.isMaxLength(256)),
|
|
7412
|
+
limit: PositiveInt.check(Schema$1.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT))
|
|
7413
|
+
});
|
|
7414
|
+
const ProjectEntryKind = Schema$1.Literals(["file", "directory"]);
|
|
7415
|
+
const ProjectEntry = Schema$1.Struct({
|
|
7416
|
+
path: TrimmedNonEmptyString,
|
|
7417
|
+
kind: ProjectEntryKind
|
|
7418
|
+
});
|
|
7419
|
+
const ProjectSearchEntriesResult = Schema$1.Struct({
|
|
7420
|
+
entries: Schema$1.Array(ProjectEntry),
|
|
7421
|
+
truncated: Schema$1.Boolean
|
|
7422
|
+
});
|
|
7423
|
+
const ProjectListEntriesInput = Schema$1.Struct({ cwd: TrimmedNonEmptyString });
|
|
7424
|
+
const ProjectListEntriesResult = Schema$1.Struct({
|
|
7425
|
+
entries: Schema$1.Array(ProjectEntry),
|
|
7426
|
+
truncated: Schema$1.Boolean
|
|
7427
|
+
});
|
|
7428
|
+
const ProjectEntriesFailure = Schema$1.Literals([
|
|
7429
|
+
"workspace_root_not_found",
|
|
7430
|
+
"workspace_root_create_failed",
|
|
7431
|
+
"workspace_root_stat_failed",
|
|
7432
|
+
"workspace_root_not_directory",
|
|
7433
|
+
"search_index_create_failed",
|
|
7434
|
+
"search_index_scan_timed_out",
|
|
7435
|
+
"search_index_search_failed"
|
|
7436
|
+
]);
|
|
7437
|
+
function decodedProjectErrorMessage(props) {
|
|
7438
|
+
if (!("message" in props)) return void 0;
|
|
7439
|
+
return typeof props.message === "string" ? props.message : void 0;
|
|
7440
|
+
}
|
|
7441
|
+
var ProjectSearchEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectSearchEntriesError", {
|
|
7442
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7443
|
+
queryLength: Schema$1.optional(NonNegativeInt),
|
|
7444
|
+
limit: Schema$1.optional(PositiveInt),
|
|
7445
|
+
failure: Schema$1.optional(ProjectEntriesFailure),
|
|
7446
|
+
normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7447
|
+
timeout: Schema$1.optional(TrimmedNonEmptyString),
|
|
7448
|
+
detail: Schema$1.optional(TrimmedNonEmptyString),
|
|
7449
|
+
message: TrimmedNonEmptyString,
|
|
7450
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7451
|
+
}) {
|
|
7452
|
+
constructor(props) {
|
|
7453
|
+
super({
|
|
7454
|
+
...props,
|
|
7455
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to search workspace entries in '${props.cwd}'.`
|
|
7456
|
+
});
|
|
7457
|
+
}
|
|
7458
|
+
};
|
|
7459
|
+
var ProjectListEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectListEntriesError", {
|
|
7460
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7461
|
+
failure: Schema$1.optional(ProjectEntriesFailure),
|
|
7462
|
+
normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7463
|
+
timeout: Schema$1.optional(TrimmedNonEmptyString),
|
|
7464
|
+
detail: Schema$1.optional(TrimmedNonEmptyString),
|
|
7465
|
+
message: TrimmedNonEmptyString,
|
|
7466
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7467
|
+
}) {
|
|
7468
|
+
constructor(props) {
|
|
7469
|
+
super({
|
|
7470
|
+
...props,
|
|
7471
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to list workspace entries in '${props.cwd}'.`
|
|
7472
|
+
});
|
|
7473
|
+
}
|
|
7474
|
+
};
|
|
7475
|
+
const ProjectReadFileInput = Schema$1.Struct({
|
|
7476
|
+
cwd: TrimmedNonEmptyString,
|
|
7477
|
+
relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_READ_FILE_PATH_MAX_LENGTH))
|
|
7478
|
+
});
|
|
7479
|
+
const ProjectReadFileResult = Schema$1.Struct({
|
|
7480
|
+
relativePath: TrimmedNonEmptyString,
|
|
7481
|
+
contents: Schema$1.String,
|
|
7482
|
+
byteLength: NonNegativeInt,
|
|
7483
|
+
truncated: Schema$1.Boolean
|
|
7484
|
+
});
|
|
7485
|
+
const ProjectFileFailure = Schema$1.Literals([
|
|
7486
|
+
"workspace_path_outside_root",
|
|
7487
|
+
"resolved_path_outside_root",
|
|
7488
|
+
"path_not_file",
|
|
7489
|
+
"binary_file",
|
|
7490
|
+
"operation_failed"
|
|
7491
|
+
]);
|
|
7492
|
+
const ProjectFileOperation = Schema$1.Literals([
|
|
7493
|
+
"realpath-workspace-root",
|
|
7494
|
+
"realpath-target",
|
|
7495
|
+
"open",
|
|
7496
|
+
"stat",
|
|
7497
|
+
"read",
|
|
7498
|
+
"close",
|
|
7499
|
+
"make-directory",
|
|
7500
|
+
"write-file"
|
|
7501
|
+
]);
|
|
7502
|
+
var ProjectReadFileError = class extends Schema$1.TaggedErrorClass()("ProjectReadFileError", {
|
|
7503
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7504
|
+
relativePath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7505
|
+
failure: Schema$1.optional(ProjectFileFailure),
|
|
7506
|
+
resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7507
|
+
resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
|
|
7508
|
+
operation: Schema$1.optional(ProjectFileOperation),
|
|
7509
|
+
operationPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7510
|
+
message: TrimmedNonEmptyString,
|
|
7511
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7512
|
+
}) {
|
|
7513
|
+
constructor(props) {
|
|
7514
|
+
super({
|
|
7515
|
+
...props,
|
|
7516
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to read workspace file '${props.relativePath}' in '${props.cwd}'.`
|
|
7517
|
+
});
|
|
7518
|
+
}
|
|
7519
|
+
};
|
|
7520
|
+
const ProjectWriteFileInput = Schema$1.Struct({
|
|
7521
|
+
cwd: TrimmedNonEmptyString,
|
|
7522
|
+
relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)),
|
|
7523
|
+
contents: Schema$1.String
|
|
7524
|
+
});
|
|
7525
|
+
const ProjectWriteFileResult = Schema$1.Struct({ relativePath: TrimmedNonEmptyString });
|
|
7526
|
+
var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWriteFileError", {
|
|
7527
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7528
|
+
relativePath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7529
|
+
failure: Schema$1.optional(ProjectFileFailure),
|
|
7530
|
+
resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7531
|
+
resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
|
|
7532
|
+
operation: Schema$1.optional(ProjectFileOperation),
|
|
7533
|
+
operationPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7534
|
+
message: TrimmedNonEmptyString,
|
|
7535
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7536
|
+
}) {
|
|
7537
|
+
constructor(props) {
|
|
7538
|
+
super({
|
|
7539
|
+
...props,
|
|
7540
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to write workspace file '${props.relativePath}' in '${props.cwd}'.`
|
|
7541
|
+
});
|
|
7542
|
+
}
|
|
7543
|
+
};
|
|
7544
|
+
//#endregion
|
|
7483
7545
|
//#region ../../packages/contracts/src/agentAsset.ts
|
|
7484
7546
|
/**
|
|
7485
7547
|
* User-scope agent assets — the things that make an agent behave the same on
|
|
@@ -12030,6 +12092,65 @@ var _036_TaskRepositoryKey_default = Effect.gen(function* () {
|
|
|
12030
12092
|
`;
|
|
12031
12093
|
});
|
|
12032
12094
|
//#endregion
|
|
12095
|
+
//#region src/persistence/Migrations/037_TaskReadableIds.ts
|
|
12096
|
+
/**
|
|
12097
|
+
* Human-readable task ids (`T-1`, `P4-12`).
|
|
12098
|
+
*
|
|
12099
|
+
* `task_sequences` holds one row per prefix with the highest number ever
|
|
12100
|
+
* handed out. It only grows: deleting a task does not return its number, so a
|
|
12101
|
+
* readable id, once uttered, never comes to mean a different task.
|
|
12102
|
+
*
|
|
12103
|
+
* Existing rows are backfilled with the default `T` prefix in creation order.
|
|
12104
|
+
* Their projects' prefixes are deliberately not consulted — a prefix lives in
|
|
12105
|
+
* the project's checked-in `p4.json`, and a migration that shells out to read
|
|
12106
|
+
* workspaces (which may no longer be on disk) is exactly what a migration
|
|
12107
|
+
* should not be. New tasks pick up their project's prefix from creation on.
|
|
12108
|
+
*/
|
|
12109
|
+
var _037_TaskReadableIds_default = Effect.gen(function* () {
|
|
12110
|
+
const sql = yield* SqlClient.SqlClient;
|
|
12111
|
+
yield* sql`
|
|
12112
|
+
CREATE TABLE IF NOT EXISTS task_sequences (
|
|
12113
|
+
prefix TEXT PRIMARY KEY,
|
|
12114
|
+
last_seq INTEGER NOT NULL
|
|
12115
|
+
)
|
|
12116
|
+
`;
|
|
12117
|
+
yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
|
|
12118
|
+
yield* sql`
|
|
12119
|
+
UPDATE tasks
|
|
12120
|
+
SET readable_id = 'T-' || (
|
|
12121
|
+
SELECT COUNT(*)
|
|
12122
|
+
FROM tasks AS earlier
|
|
12123
|
+
WHERE earlier.created_at < tasks.created_at
|
|
12124
|
+
OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
|
|
12125
|
+
)
|
|
12126
|
+
WHERE readable_id IS NULL
|
|
12127
|
+
`;
|
|
12128
|
+
yield* sql`
|
|
12129
|
+
INSERT INTO task_sequences (prefix, last_seq)
|
|
12130
|
+
SELECT 'T', COUNT(*) FROM tasks
|
|
12131
|
+
WHERE true
|
|
12132
|
+
ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
|
|
12133
|
+
`;
|
|
12134
|
+
yield* sql`
|
|
12135
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
|
|
12136
|
+
ON tasks(readable_id)
|
|
12137
|
+
`;
|
|
12138
|
+
});
|
|
12139
|
+
//#endregion
|
|
12140
|
+
//#region src/persistence/Migrations/038_TaskOrderKeys.ts
|
|
12141
|
+
/**
|
|
12142
|
+
* Manual board position (`order_key`): a lexicographic fractional key written
|
|
12143
|
+
* when a card is dragged to a position, so one move rewrites one row.
|
|
12144
|
+
*
|
|
12145
|
+
* Existing rows are deliberately not backfilled. A null key means "never placed
|
|
12146
|
+
* by hand", and the board's comparator falls back to the old priority/recency
|
|
12147
|
+
* order for those - so a board nobody has dragged on looks exactly as it did
|
|
12148
|
+
* before this migration.
|
|
12149
|
+
*/
|
|
12150
|
+
var _038_TaskOrderKeys_default = Effect.gen(function* () {
|
|
12151
|
+
yield* (yield* SqlClient.SqlClient)`ALTER TABLE tasks ADD COLUMN order_key TEXT`;
|
|
12152
|
+
});
|
|
12153
|
+
//#endregion
|
|
12033
12154
|
//#region src/persistence/Migrations.ts
|
|
12034
12155
|
/**
|
|
12035
12156
|
* MigrationsLive - Migration runner with inline loader
|
|
@@ -12230,6 +12351,16 @@ const migrationEntries = [
|
|
|
12230
12351
|
36,
|
|
12231
12352
|
"TaskRepositoryKey",
|
|
12232
12353
|
_036_TaskRepositoryKey_default
|
|
12354
|
+
],
|
|
12355
|
+
[
|
|
12356
|
+
37,
|
|
12357
|
+
"TaskReadableIds",
|
|
12358
|
+
_037_TaskReadableIds_default
|
|
12359
|
+
],
|
|
12360
|
+
[
|
|
12361
|
+
38,
|
|
12362
|
+
"TaskOrderKeys",
|
|
12363
|
+
_038_TaskOrderKeys_default
|
|
12233
12364
|
]
|
|
12234
12365
|
];
|
|
12235
12366
|
const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -13853,6 +13984,7 @@ const TaskListQuery = Schema$1.Struct({
|
|
|
13853
13984
|
});
|
|
13854
13985
|
const TASK_COLUMNS = `
|
|
13855
13986
|
task_id AS "taskId",
|
|
13987
|
+
readable_id AS "readableId",
|
|
13856
13988
|
project_id AS "projectId",
|
|
13857
13989
|
repository_key AS "repositoryKey",
|
|
13858
13990
|
thread_id AS "threadId",
|
|
@@ -13862,6 +13994,7 @@ const TASK_COLUMNS = `
|
|
|
13862
13994
|
priority,
|
|
13863
13995
|
assignee,
|
|
13864
13996
|
labels_json AS "labels",
|
|
13997
|
+
order_key AS "orderKey",
|
|
13865
13998
|
created_at AS "createdAt",
|
|
13866
13999
|
updated_at AS "updatedAt"
|
|
13867
14000
|
`;
|
|
@@ -13874,6 +14007,7 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13874
14007
|
execute: (row) => sql`
|
|
13875
14008
|
INSERT INTO tasks (
|
|
13876
14009
|
task_id,
|
|
14010
|
+
readable_id,
|
|
13877
14011
|
project_id,
|
|
13878
14012
|
repository_key,
|
|
13879
14013
|
thread_id,
|
|
@@ -13883,11 +14017,13 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13883
14017
|
priority,
|
|
13884
14018
|
assignee,
|
|
13885
14019
|
labels_json,
|
|
14020
|
+
order_key,
|
|
13886
14021
|
created_at,
|
|
13887
14022
|
updated_at
|
|
13888
14023
|
)
|
|
13889
14024
|
VALUES (
|
|
13890
14025
|
${row.taskId},
|
|
14026
|
+
${row.readableId},
|
|
13891
14027
|
${row.projectId},
|
|
13892
14028
|
${row.repositoryKey},
|
|
13893
14029
|
${row.threadId},
|
|
@@ -13897,11 +14033,13 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13897
14033
|
${row.priority},
|
|
13898
14034
|
${row.assignee},
|
|
13899
14035
|
${JSON.stringify(row.labels)},
|
|
14036
|
+
${row.orderKey},
|
|
13900
14037
|
${row.createdAt},
|
|
13901
14038
|
${row.updatedAt}
|
|
13902
14039
|
)
|
|
13903
14040
|
ON CONFLICT (task_id)
|
|
13904
14041
|
DO UPDATE SET
|
|
14042
|
+
readable_id = excluded.readable_id,
|
|
13905
14043
|
project_id = excluded.project_id,
|
|
13906
14044
|
repository_key = excluded.repository_key,
|
|
13907
14045
|
thread_id = excluded.thread_id,
|
|
@@ -13911,6 +14049,7 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13911
14049
|
priority = excluded.priority,
|
|
13912
14050
|
assignee = excluded.assignee,
|
|
13913
14051
|
labels_json = excluded.labels_json,
|
|
14052
|
+
order_key = excluded.order_key,
|
|
13914
14053
|
created_at = excluded.created_at,
|
|
13915
14054
|
updated_at = excluded.updated_at
|
|
13916
14055
|
`
|
|
@@ -13920,6 +14059,26 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13920
14059
|
Result: TaskDbRow,
|
|
13921
14060
|
execute: ({ taskId }) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE task_id = ${taskId}`
|
|
13922
14061
|
});
|
|
14062
|
+
const getTaskRowByReadableId = SqlSchema.findOneOption({
|
|
14063
|
+
Request: Schema$1.String,
|
|
14064
|
+
Result: TaskDbRow,
|
|
14065
|
+
execute: (readableId) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE readable_id = ${readableId}`
|
|
14066
|
+
});
|
|
14067
|
+
/**
|
|
14068
|
+
* Take the next number for a prefix, creating the sequence at 1 on first
|
|
14069
|
+
* use. The row is never reset or decremented - a deleted task's number is
|
|
14070
|
+
* gone - which is what makes a readable id safe to say out loud.
|
|
14071
|
+
*/
|
|
14072
|
+
const allocateReadableIdSeq = SqlSchema.findAll({
|
|
14073
|
+
Request: Schema$1.Struct({ prefix: Schema$1.String }),
|
|
14074
|
+
Result: Schema$1.Struct({ lastSeq: Schema$1.Int }),
|
|
14075
|
+
execute: ({ prefix }) => sql`
|
|
14076
|
+
INSERT INTO task_sequences (prefix, last_seq)
|
|
14077
|
+
VALUES (${prefix}, 1)
|
|
14078
|
+
ON CONFLICT (prefix) DO UPDATE SET last_seq = last_seq + 1
|
|
14079
|
+
RETURNING last_seq AS "lastSeq"
|
|
14080
|
+
`
|
|
14081
|
+
});
|
|
13923
14082
|
const listTaskRows = SqlSchema.findAll({
|
|
13924
14083
|
Request: TaskListQuery,
|
|
13925
14084
|
Result: TaskDbRow,
|
|
@@ -13942,7 +14101,31 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13942
14101
|
type: "upserted",
|
|
13943
14102
|
task: row
|
|
13944
14103
|
})));
|
|
14104
|
+
/**
|
|
14105
|
+
* The allocation and the insert share one transaction on purpose: a number
|
|
14106
|
+
* taken outside it would either be lost on a failed insert (a gap nobody
|
|
14107
|
+
* caused) or, worse, handed to two concurrent creates.
|
|
14108
|
+
*/
|
|
14109
|
+
const create = (row, readableIdPrefix) => sql.withTransaction(Effect.gen(function* () {
|
|
14110
|
+
const prefix = readableIdPrefix.toUpperCase();
|
|
14111
|
+
const seq = (yield* allocateReadableIdSeq({ prefix }))[0]?.lastSeq;
|
|
14112
|
+
if (seq === void 0) return yield* new PersistenceSqlError({
|
|
14113
|
+
operation: "TaskRepository.create:allocate",
|
|
14114
|
+
detail: "the sequence upsert returned no row",
|
|
14115
|
+
cause: null
|
|
14116
|
+
});
|
|
14117
|
+
const task = {
|
|
14118
|
+
...row,
|
|
14119
|
+
readableId: `${prefix}-${seq}`
|
|
14120
|
+
};
|
|
14121
|
+
yield* upsertTaskRow(task);
|
|
14122
|
+
return task;
|
|
14123
|
+
})).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.create:query")), Effect.tap((task) => publish({
|
|
14124
|
+
type: "upserted",
|
|
14125
|
+
task
|
|
14126
|
+
})));
|
|
13945
14127
|
const getById = (input) => getTaskRow(input).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getById:query")));
|
|
14128
|
+
const getByReadableId = (readableId) => getTaskRowByReadableId(readableId.toUpperCase()).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getByReadableId:query")));
|
|
13946
14129
|
const list = (filter) => listTaskRows({
|
|
13947
14130
|
projectId: filter.projectId ?? null,
|
|
13948
14131
|
repositoryKey: filter.repositoryKey ?? null,
|
|
@@ -13964,6 +14147,7 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13964
14147
|
...input.priority !== void 0 ? { priority: input.priority } : {},
|
|
13965
14148
|
...input.assignee !== void 0 ? { assignee: input.assignee } : {},
|
|
13966
14149
|
...input.labels !== void 0 ? { labels: input.labels } : {},
|
|
14150
|
+
...input.orderKey !== void 0 ? { orderKey: input.orderKey } : {},
|
|
13967
14151
|
updatedAt
|
|
13968
14152
|
};
|
|
13969
14153
|
yield* upsertTaskRow(updated);
|
|
@@ -14001,6 +14185,7 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
14001
14185
|
...input.patch.priority !== void 0 ? { priority: input.patch.priority } : {},
|
|
14002
14186
|
...input.patch.assignee !== void 0 ? { assignee: input.patch.assignee } : {},
|
|
14003
14187
|
...input.patch.labels !== void 0 ? { labels: input.patch.labels } : {},
|
|
14188
|
+
...input.patch.orderKey !== void 0 ? { orderKey: input.patch.orderKey } : {},
|
|
14004
14189
|
updatedAt
|
|
14005
14190
|
};
|
|
14006
14191
|
yield* upsertTaskRow(updated);
|
|
@@ -14017,10 +14202,12 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
14017
14202
|
taskId: input.taskId
|
|
14018
14203
|
})));
|
|
14019
14204
|
return {
|
|
14205
|
+
create,
|
|
14020
14206
|
upsert,
|
|
14021
14207
|
patch,
|
|
14022
14208
|
patchIfStatus,
|
|
14023
14209
|
getById,
|
|
14210
|
+
getByReadableId,
|
|
14024
14211
|
list,
|
|
14025
14212
|
deleteById,
|
|
14026
14213
|
streamChanges: Stream.fromPubSub(changes),
|
|
@@ -14109,12 +14296,13 @@ const getTaskRoute = HttpRouter.add("GET", "/tasks/:taskId", respondToHubFailure
|
|
|
14109
14296
|
yield* authenticateHubRequest();
|
|
14110
14297
|
const params = yield* HttpRouter.params;
|
|
14111
14298
|
const taskId = TaskId.make(params.taskId ?? "");
|
|
14112
|
-
const
|
|
14299
|
+
const tasks = yield* TaskRepository;
|
|
14300
|
+
const found = isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
|
|
14113
14301
|
return Option.isNone(found) ? notFound(taskId) : HttpServerResponse.jsonUnsafe(found.value);
|
|
14114
14302
|
})));
|
|
14115
14303
|
const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Effect.gen(function* () {
|
|
14116
14304
|
yield* authenticateHubRequest();
|
|
14117
|
-
const input = yield* decodeOrInvalid(
|
|
14305
|
+
const input = yield* decodeOrInvalid(HubTaskCreateInput, "Request body does not match HubTaskCreateInput.")(yield* readJsonBody());
|
|
14118
14306
|
const tasks = yield* TaskRepository;
|
|
14119
14307
|
const crypto = yield* Crypto.Crypto;
|
|
14120
14308
|
const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
@@ -14130,11 +14318,12 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
|
|
|
14130
14318
|
priority: input.priority ?? "none",
|
|
14131
14319
|
assignee: input.assignee ?? null,
|
|
14132
14320
|
labels: input.labels ?? [],
|
|
14321
|
+
orderKey: input.orderKey ?? null,
|
|
14133
14322
|
createdAt: timestamp,
|
|
14134
14323
|
updatedAt: timestamp
|
|
14135
14324
|
};
|
|
14136
|
-
yield* tasks.
|
|
14137
|
-
return HttpServerResponse.jsonUnsafe(
|
|
14325
|
+
const task = yield* tasks.create(row, input.readableIdPrefix ?? "T");
|
|
14326
|
+
return HttpServerResponse.jsonUnsafe(task, { status: 201 });
|
|
14138
14327
|
})));
|
|
14139
14328
|
const updateTaskRoute = HttpRouter.add("PATCH", "/tasks/:taskId", respondToHubFailures(Effect.gen(function* () {
|
|
14140
14329
|
yield* authenticateHubRequest();
|
|
@@ -14372,6 +14561,60 @@ var _003_AgentAssets_default = Effect.gen(function* () {
|
|
|
14372
14561
|
`;
|
|
14373
14562
|
});
|
|
14374
14563
|
//#endregion
|
|
14564
|
+
//#region src/hub/Migrations/004_TaskReadableIds.ts
|
|
14565
|
+
/**
|
|
14566
|
+
* Human-readable task ids (`T-1`, `P4-12`).
|
|
14567
|
+
*
|
|
14568
|
+
* `task_sequences` holds one row per prefix with the highest number ever
|
|
14569
|
+
* handed out. It only grows: deleting a task does not return its number, so a
|
|
14570
|
+
* readable id, once uttered, never comes to mean a different task.
|
|
14571
|
+
*
|
|
14572
|
+
* Existing rows are backfilled with the default `T` prefix in creation order.
|
|
14573
|
+
* Prefixes cannot be consulted here at all: they live in each project's
|
|
14574
|
+
* checked-in `p4.json`, and the hub has no checkouts. New tasks arrive with a
|
|
14575
|
+
* prefix resolved by the submitting environment server.
|
|
14576
|
+
*/
|
|
14577
|
+
var _004_TaskReadableIds_default = Effect.gen(function* () {
|
|
14578
|
+
const sql = yield* SqlClient.SqlClient;
|
|
14579
|
+
yield* sql`
|
|
14580
|
+
CREATE TABLE IF NOT EXISTS task_sequences (
|
|
14581
|
+
prefix TEXT PRIMARY KEY,
|
|
14582
|
+
last_seq INTEGER NOT NULL
|
|
14583
|
+
)
|
|
14584
|
+
`;
|
|
14585
|
+
yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
|
|
14586
|
+
yield* sql`
|
|
14587
|
+
UPDATE tasks
|
|
14588
|
+
SET readable_id = 'T-' || (
|
|
14589
|
+
SELECT COUNT(*)
|
|
14590
|
+
FROM tasks AS earlier
|
|
14591
|
+
WHERE earlier.created_at < tasks.created_at
|
|
14592
|
+
OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
|
|
14593
|
+
)
|
|
14594
|
+
WHERE readable_id IS NULL
|
|
14595
|
+
`;
|
|
14596
|
+
yield* sql`
|
|
14597
|
+
INSERT INTO task_sequences (prefix, last_seq)
|
|
14598
|
+
SELECT 'T', COUNT(*) FROM tasks
|
|
14599
|
+
WHERE true
|
|
14600
|
+
ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
|
|
14601
|
+
`;
|
|
14602
|
+
yield* sql`
|
|
14603
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
|
|
14604
|
+
ON tasks(readable_id)
|
|
14605
|
+
`;
|
|
14606
|
+
});
|
|
14607
|
+
//#endregion
|
|
14608
|
+
//#region src/hub/Migrations/005_TaskOrderKeys.ts
|
|
14609
|
+
/**
|
|
14610
|
+
* Manual board position (`order_key`), mirroring the environment server's
|
|
14611
|
+
* migration 038: a lexicographic fractional key written when a card is dragged
|
|
14612
|
+
* to a position. No backfill - null falls back to priority/recency order.
|
|
14613
|
+
*/
|
|
14614
|
+
var _005_TaskOrderKeys_default = Effect.gen(function* () {
|
|
14615
|
+
yield* (yield* SqlClient.SqlClient)`ALTER TABLE tasks ADD COLUMN order_key TEXT`;
|
|
14616
|
+
});
|
|
14617
|
+
//#endregion
|
|
14375
14618
|
//#region src/hub/Migrations.ts
|
|
14376
14619
|
/**
|
|
14377
14620
|
* Hub migrations.
|
|
@@ -14397,6 +14640,16 @@ const hubMigrationEntries = [
|
|
|
14397
14640
|
3,
|
|
14398
14641
|
"AgentAssets",
|
|
14399
14642
|
_003_AgentAssets_default
|
|
14643
|
+
],
|
|
14644
|
+
[
|
|
14645
|
+
4,
|
|
14646
|
+
"TaskReadableIds",
|
|
14647
|
+
_004_TaskReadableIds_default
|
|
14648
|
+
],
|
|
14649
|
+
[
|
|
14650
|
+
5,
|
|
14651
|
+
"TaskOrderKeys",
|
|
14652
|
+
_005_TaskOrderKeys_default
|
|
14400
14653
|
]
|
|
14401
14654
|
];
|
|
14402
14655
|
const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -32038,6 +32291,8 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
|
|
|
32038
32291
|
* presentation and is not.
|
|
32039
32292
|
*
|
|
32040
32293
|
* The id and timestamp are passed in so this stays pure and holds no clock.
|
|
32294
|
+
* The readable id is absent on purpose: the store allocates it at insert time,
|
|
32295
|
+
* inside the same transaction as the sequence bump.
|
|
32041
32296
|
*/
|
|
32042
32297
|
const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
|
|
32043
32298
|
taskId,
|
|
@@ -32050,6 +32305,7 @@ const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
|
|
|
32050
32305
|
priority: input.priority ?? "none",
|
|
32051
32306
|
assignee: input.assignee ?? null,
|
|
32052
32307
|
labels: input.labels ?? [],
|
|
32308
|
+
orderKey: null,
|
|
32053
32309
|
createdAt: timestamp,
|
|
32054
32310
|
updatedAt: timestamp
|
|
32055
32311
|
});
|
|
@@ -32073,6 +32329,23 @@ const resolveTaskRepositoryKey = Effect.fn("taskBoard.resolveRepositoryKey")(fun
|
|
|
32073
32329
|
return (yield* (yield* RepositoryIdentityResolver).resolve(project.value.workspaceRoot))?.canonicalKey ?? null;
|
|
32074
32330
|
});
|
|
32075
32331
|
/**
|
|
32332
|
+
* Resolve the readable-id prefix for a task's project.
|
|
32333
|
+
*
|
|
32334
|
+
* The prefix lives in the project's checked-in `p4.json` (`taskPrefix`), not in
|
|
32335
|
+
* this server's database: it is a property of the repository, agreed on by
|
|
32336
|
+
* everyone who clones it, exactly like the scripts beside it. A task with no
|
|
32337
|
+
* project, a removed project, or a `p4.json` without the field all fall back to
|
|
32338
|
+
* the default `T` - filing the task always beats refusing it over a label.
|
|
32339
|
+
*/
|
|
32340
|
+
const resolveTaskReadableIdPrefix = Effect.fn("taskBoard.resolveReadableIdPrefix")(function* (projectId) {
|
|
32341
|
+
if (projectId == null) return "T";
|
|
32342
|
+
const project = yield* (yield* ProjectionProjectRepository).getById({ projectId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
32343
|
+
if (Option.isNone(project)) return "T";
|
|
32344
|
+
const file = yield* (yield* P4ProjectFileLoader).load(project.value.workspaceRoot);
|
|
32345
|
+
if (Option.isNone(file) || file.value.taskPrefix === void 0) return "T";
|
|
32346
|
+
return file.value.taskPrefix.toUpperCase();
|
|
32347
|
+
});
|
|
32348
|
+
/**
|
|
32076
32349
|
* Everything that decides whether a task can be started, as one pure function.
|
|
32077
32350
|
*
|
|
32078
32351
|
* Kept out of the RPC handler because the three refusals are the whole of the
|
|
@@ -42697,9 +42970,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
|
|
|
42697
42970
|
[WS_METHODS.tasksList]: (filter) => observeRpcEffect$1(WS_METHODS.tasksList, taskRepository.list(filter).pipe(Effect.catch(toTaskStoreError("tasks.list")), Effect.map((tasks) => ({ tasks }))), { "rpc.aggregate": "tasks" }),
|
|
42698
42971
|
[WS_METHODS.tasksGet]: ({ taskId }) => observeRpcEffect$1(WS_METHODS.tasksGet, taskRepository.getById({ taskId }).pipe(Effect.catch(toTaskStoreError("tasks.get")), Effect.map((found) => ({ task: Option.getOrNull(found) }))), { "rpc.aggregate": "tasks" }),
|
|
42699
42972
|
[WS_METHODS.tasksCreate]: (input) => observeRpcEffect$1(WS_METHODS.tasksCreate, Effect.gen(function* () {
|
|
42700
|
-
const
|
|
42701
|
-
yield*
|
|
42702
|
-
|
|
42973
|
+
const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
42974
|
+
const timestamp = yield* nowIso$5;
|
|
42975
|
+
const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
|
|
42976
|
+
const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
|
|
42977
|
+
const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
|
|
42978
|
+
return { task: yield* taskRepository.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("tasks.create"))) };
|
|
42703
42979
|
}), { "rpc.aggregate": "tasks" }),
|
|
42704
42980
|
[WS_METHODS.tasksUpdate]: (input) => observeRpcEffect$1(WS_METHODS.tasksUpdate, Effect.gen(function* () {
|
|
42705
42981
|
const timestamp = yield* nowIso$5;
|
|
@@ -43010,12 +43286,13 @@ const websocketRpcRouteLayer = Layer.unwrap(Effect.gen(function* () {
|
|
|
43010
43286
|
const serverSelfUpdate = yield* ServerSelfUpdate;
|
|
43011
43287
|
const taskRepository = yield* TaskRepository;
|
|
43012
43288
|
const projectionProjects = yield* ProjectionProjectRepository;
|
|
43289
|
+
const p4ProjectFileLoader = yield* P4ProjectFileLoader;
|
|
43013
43290
|
return HttpRouter.add("GET", "/ws", Effect.gen(function* () {
|
|
43014
43291
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
43015
43292
|
const serverAuth = yield* EnvironmentAuth;
|
|
43016
43293
|
const sessions = yield* SessionStore;
|
|
43017
43294
|
const session = yield* serverAuth.authenticateWebSocketUpgrade(request).pipe(Effect.catchIf(isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid(serverAuthCredentialReason(error))), Effect.catchIf(isServerAuthInternalError, (error) => failEnvironmentInternal("internal_error", error)));
|
|
43018
|
-
const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { disableTracing: true }).pipe(Effect.provide(makeWsRpcLayer(session, previewAutomationBroker).pipe(Layer.provideMerge(RpcSerialization.layerJson), Layer.provide(layer$33), Layer.provide(Layer.succeed(ServerSelfUpdate, serverSelfUpdate)), Layer.provide(Layer.succeed(TaskRepository, taskRepository)), Layer.provide(Layer.succeed(ProjectionProjectRepository, projectionProjects)), Layer.provide(layer$5.pipe(Layer.provide(layer$14.pipe(Layer.provide(Layer.mergeAll(layer$22, layer$20, layer$18, layer$16)), Layer.provideMerge(layer$38), Layer.provide(layer$36.pipe(Layer.provide(layer$37))))), Layer.provide(layer$39))))));
|
|
43295
|
+
const rpcWebSocketHttpEffect = yield* RpcServer.toHttpEffectWebsocket(WsRpcGroup, { disableTracing: true }).pipe(Effect.provide(makeWsRpcLayer(session, previewAutomationBroker).pipe(Layer.provideMerge(RpcSerialization.layerJson), Layer.provide(layer$33), Layer.provide(Layer.succeed(ServerSelfUpdate, serverSelfUpdate)), Layer.provide(Layer.succeed(TaskRepository, taskRepository)), Layer.provide(Layer.succeed(ProjectionProjectRepository, projectionProjects)), Layer.provide(Layer.succeed(P4ProjectFileLoader, p4ProjectFileLoader)), Layer.provide(layer$5.pipe(Layer.provide(layer$14.pipe(Layer.provide(Layer.mergeAll(layer$22, layer$20, layer$18, layer$16)), Layer.provideMerge(layer$38), Layer.provide(layer$36.pipe(Layer.provide(layer$37))))), Layer.provide(layer$39))))));
|
|
43019
43296
|
return yield* Effect.acquireUseRelease(sessions.markConnected(session.sessionId), () => rpcWebSocketHttpEffect, () => sessions.markDisconnected(session.sessionId));
|
|
43020
43297
|
}).pipe(Effect.catchTags({
|
|
43021
43298
|
EnvironmentAuthInvalidError: HttpServerRespondable.toResponse,
|
|
@@ -43120,17 +43397,16 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
|
|
|
43120
43397
|
return Option.some(yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("getById", "the response did not match the contract"))));
|
|
43121
43398
|
});
|
|
43122
43399
|
/**
|
|
43123
|
-
*
|
|
43124
|
-
* store
|
|
43125
|
-
*
|
|
43126
|
-
*
|
|
43127
|
-
*
|
|
43128
|
-
*
|
|
43129
|
-
* that quietly would let two servers mint colliding ids.
|
|
43400
|
+
* Both writes here POST the hub's create route, and that is a real
|
|
43401
|
+
* difference from the local store: the hub mints its own ids, timestamps and
|
|
43402
|
+
* readable id, so a row written through this comes back with the hub's
|
|
43403
|
+
* version of all three. The caller's `taskId` is not carried - honouring it
|
|
43404
|
+
* would need a hub route that takes one, and inventing that quietly would
|
|
43405
|
+
* let two servers mint colliding ids.
|
|
43130
43406
|
*/
|
|
43131
|
-
const
|
|
43132
|
-
const settings = yield* requireSettings(
|
|
43133
|
-
const response = yield* send(
|
|
43407
|
+
const postCreate = (operation, row, readableIdPrefix) => Effect.gen(function* () {
|
|
43408
|
+
const settings = yield* requireSettings(operation);
|
|
43409
|
+
const response = yield* send(operation, settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(`${settings.baseUrl}/tasks`), {
|
|
43134
43410
|
...row.projectId === null ? {} : { projectId: row.projectId },
|
|
43135
43411
|
...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
|
|
43136
43412
|
...row.threadId === null ? {} : { threadId: row.threadId },
|
|
@@ -43139,15 +43415,25 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
|
|
|
43139
43415
|
status: row.status,
|
|
43140
43416
|
priority: row.priority,
|
|
43141
43417
|
...row.assignee === null ? {} : { assignee: row.assignee },
|
|
43142
|
-
labels: row.labels
|
|
43418
|
+
labels: row.labels,
|
|
43419
|
+
...row.orderKey === null ? {} : { orderKey: row.orderKey },
|
|
43420
|
+
...readableIdPrefix === void 0 ? {} : { readableIdPrefix }
|
|
43143
43421
|
}));
|
|
43144
|
-
if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable(
|
|
43145
|
-
const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(
|
|
43422
|
+
if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable(operation, `status ${response.status}`);
|
|
43423
|
+
const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(operation, "the response did not match the contract")));
|
|
43146
43424
|
yield* publish({
|
|
43147
43425
|
type: "upserted",
|
|
43148
43426
|
task: created
|
|
43149
43427
|
});
|
|
43428
|
+
return created;
|
|
43150
43429
|
});
|
|
43430
|
+
const create = (row, readableIdPrefix) => postCreate("create", row, readableIdPrefix);
|
|
43431
|
+
const upsert = (row) => postCreate("upsert", row).pipe(Effect.asVoid);
|
|
43432
|
+
/**
|
|
43433
|
+
* Served by the same hub route as `getById`: readable ids and UUIDs are
|
|
43434
|
+
* disjoint by shape, so the hub dispatches on what it is handed.
|
|
43435
|
+
*/
|
|
43436
|
+
const getByReadableId = (readableId) => getById({ taskId: TaskId.make(readableId) });
|
|
43151
43437
|
const patch = (input) => Effect.gen(function* () {
|
|
43152
43438
|
const settings = yield* requireSettings("patch");
|
|
43153
43439
|
const { taskId, ...fields } = input;
|
|
@@ -43222,10 +43508,12 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
|
|
|
43222
43508
|
}).pipe(Effect.ignoreCause({ log: true }));
|
|
43223
43509
|
yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL)))).pipe(Effect.forkScoped);
|
|
43224
43510
|
return {
|
|
43511
|
+
create,
|
|
43225
43512
|
upsert,
|
|
43226
43513
|
patch,
|
|
43227
43514
|
patchIfStatus,
|
|
43228
43515
|
getById,
|
|
43516
|
+
getByReadableId,
|
|
43229
43517
|
list,
|
|
43230
43518
|
deleteById,
|
|
43231
43519
|
streamChanges: Stream.fromPubSub(changes),
|
|
@@ -43275,10 +43563,12 @@ const makeRoutedTaskRepository = Effect.gen(function* () {
|
|
|
43275
43563
|
*/
|
|
43276
43564
|
const pick = usingHub.pipe(Effect.map((hub) => hub ? remote : local));
|
|
43277
43565
|
return {
|
|
43566
|
+
create: (row, readableIdPrefix) => pick.pipe(Effect.flatMap((repository) => repository.create(row, readableIdPrefix))),
|
|
43278
43567
|
upsert: (row) => pick.pipe(Effect.flatMap((repository) => repository.upsert(row))),
|
|
43279
43568
|
patch: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patch(input, updatedAt))),
|
|
43280
43569
|
patchIfStatus: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patchIfStatus(input, updatedAt))),
|
|
43281
43570
|
getById: (input) => pick.pipe(Effect.flatMap((repository) => repository.getById(input))),
|
|
43571
|
+
getByReadableId: (readableId) => pick.pipe(Effect.flatMap((repository) => repository.getByReadableId(readableId))),
|
|
43282
43572
|
list: (filter) => pick.pipe(Effect.flatMap((repository) => repository.list(filter))),
|
|
43283
43573
|
deleteById: (input) => pick.pipe(Effect.flatMap((repository) => repository.deleteById(input))),
|
|
43284
43574
|
get streamChanges() {
|
|
@@ -81083,8 +81373,15 @@ const writeDependencies = [
|
|
|
81083
81373
|
ProjectionProjectRepository,
|
|
81084
81374
|
RepositoryIdentityResolver
|
|
81085
81375
|
];
|
|
81086
|
-
/**
|
|
81087
|
-
|
|
81376
|
+
/**
|
|
81377
|
+
* `task_create` also mints the task id (crypto) and resolves the readable-id
|
|
81378
|
+
* prefix from the project's checked-in `p4.json` (the file loader).
|
|
81379
|
+
*/
|
|
81380
|
+
const createDependencies = [
|
|
81381
|
+
...writeDependencies,
|
|
81382
|
+
Crypto.Crypto,
|
|
81383
|
+
P4ProjectFileLoader
|
|
81384
|
+
];
|
|
81088
81385
|
const TaskListResult = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
|
|
81089
81386
|
const TaskListTool = Tool.make("task_list", {
|
|
81090
81387
|
description: "List tasks on the board, most recently updated first. Every filter is optional and they combine with AND; omit all of them to list every task. Use this to find work rather than guessing task ids.",
|
|
@@ -81094,7 +81391,7 @@ const TaskListTool = Tool.make("task_list", {
|
|
|
81094
81391
|
dependencies
|
|
81095
81392
|
}).annotate(Tool.Title, "List tasks").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
|
|
81096
81393
|
const TaskGetTool = Tool.make("task_get", {
|
|
81097
|
-
description: "Read a single task by id, including its body, labels and linked thread.",
|
|
81394
|
+
description: "Read a single task by id, including its body, labels and linked thread. Accepts either the UUID or the readable id (e.g. T-3, P4-12).",
|
|
81098
81395
|
parameters: TaskGetInput,
|
|
81099
81396
|
success: Task,
|
|
81100
81397
|
failure: TaskToolError,
|
|
@@ -81144,6 +81441,15 @@ const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, Tas
|
|
|
81144
81441
|
//#endregion
|
|
81145
81442
|
//#region src/mcp/toolkits/tasks/handlers.ts
|
|
81146
81443
|
const nowIso$1 = Effect.map(DateTime.now, DateTime.formatIso);
|
|
81444
|
+
/**
|
|
81445
|
+
* Agents reference tasks by whichever id they have seen, so any `taskId`
|
|
81446
|
+
* parameter accepts the readable id (`P4-12`) as well as the UUID. The two are
|
|
81447
|
+
* disjoint by shape, which is what makes the dispatch safe.
|
|
81448
|
+
*/
|
|
81449
|
+
const findTask = (taskId) => Effect.gen(function* () {
|
|
81450
|
+
const tasks = yield* TaskRepository;
|
|
81451
|
+
return isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
|
|
81452
|
+
});
|
|
81147
81453
|
const TaskToolkitHandlersLive = TaskToolkit.toLayer({
|
|
81148
81454
|
task_list: (input) => Effect.gen(function* () {
|
|
81149
81455
|
yield* requireTaskCapability();
|
|
@@ -81151,7 +81457,7 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
|
|
|
81151
81457
|
}),
|
|
81152
81458
|
task_get: (input) => Effect.gen(function* () {
|
|
81153
81459
|
yield* requireTaskCapability();
|
|
81154
|
-
const found = yield* (
|
|
81460
|
+
const found = yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_get")));
|
|
81155
81461
|
if (Option.isNone(found)) return yield* new TaskNotFoundError({ taskId: input.taskId });
|
|
81156
81462
|
return found.value;
|
|
81157
81463
|
}),
|
|
@@ -81167,16 +81473,25 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
|
|
|
81167
81473
|
yield* requireTaskCapability();
|
|
81168
81474
|
const tasks = yield* TaskRepository;
|
|
81169
81475
|
const crypto = yield* Crypto.Crypto;
|
|
81170
|
-
const
|
|
81171
|
-
yield*
|
|
81172
|
-
|
|
81476
|
+
const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
81477
|
+
const timestamp = yield* nowIso$1;
|
|
81478
|
+
const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
|
|
81479
|
+
const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
|
|
81480
|
+
const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
|
|
81481
|
+
return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));
|
|
81173
81482
|
}),
|
|
81174
81483
|
task_update: (input) => Effect.gen(function* () {
|
|
81175
81484
|
yield* requireTaskCapability();
|
|
81176
81485
|
const tasks = yield* TaskRepository;
|
|
81177
81486
|
const timestamp = yield* nowIso$1;
|
|
81178
|
-
const
|
|
81487
|
+
const taskId = isReadableTaskId(input.taskId) ? Option.getOrNull(yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_update"))))?.taskId : input.taskId;
|
|
81488
|
+
if (taskId === void 0) return yield* new TaskNotFoundError({ taskId: input.taskId });
|
|
81489
|
+
const patch = input.projectId === void 0 ? {
|
|
81490
|
+
...input,
|
|
81491
|
+
taskId
|
|
81492
|
+
} : {
|
|
81179
81493
|
...input,
|
|
81494
|
+
taskId,
|
|
81180
81495
|
repositoryKey: yield* resolveTaskRepositoryKey(input.projectId)
|
|
81181
81496
|
};
|
|
81182
81497
|
const patched = yield* tasks.patch(patch, timestamp).pipe(Effect.catch(toTaskStoreError("task_update")));
|
|
@@ -84890,7 +85205,7 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(Checkpo
|
|
|
84890
85205
|
*/
|
|
84891
85206
|
const AssetSyncLive = layer$54.pipe(Layer.provideMerge(layer$55), Layer.provideMerge(layer$56));
|
|
84892
85207
|
const RuntimeServicesLive = layer$44.pipe(Layer.provideMerge(AssetSyncLive), Layer.provideMerge(RuntimeDependenciesLive));
|
|
84893
|
-
const makeRoutesLayer = Layer.mergeAll(Layer.mergeAll(HttpApiBuilder.layer(EnvironmentHttpApi).pipe(Layer.provide(authHttpApiLayer), Layer.provide(orchestrationHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer)), otlpTracesProxyRouteLayer, assetRouteLayer, staticAndDevRouteLayer, websocketRpcRouteLayer), layer.pipe(Layer.provide(layer$2))).pipe(Layer.provide(RoutedTaskRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(ProjectionProjectRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(layer$28), Layer.provide(layer$46), Layer.provide(browserApiCorsLayer), Layer.provide(httpCompressionLayer));
|
|
85208
|
+
const makeRoutesLayer = Layer.mergeAll(Layer.mergeAll(HttpApiBuilder.layer(EnvironmentHttpApi).pipe(Layer.provide(authHttpApiLayer), Layer.provide(orchestrationHttpApiLayer), Layer.provide(serverEnvironmentHttpApiLayer), Layer.provide(environmentAuthenticatedAuthLayer)), otlpTracesProxyRouteLayer, assetRouteLayer, staticAndDevRouteLayer, websocketRpcRouteLayer), layer.pipe(Layer.provide(layer$2))).pipe(Layer.provide(RoutedTaskRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(ProjectionProjectRepositoryLive.pipe(Layer.provide(PersistenceLayerLive))), Layer.provide(layer$42), Layer.provide(layer$28), Layer.provide(layer$46), Layer.provide(browserApiCorsLayer), Layer.provide(httpCompressionLayer));
|
|
84894
85209
|
const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
|
|
84895
85210
|
const config = yield* ServerConfig$1;
|
|
84896
85211
|
yield* fixPath();
|