@p4code/cli 0.1.7 → 0.1.8
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 +448 -201
- package/dist/client/assets/{DiffPanel-B_RUbYdJ.js → DiffPanel-D4tNpljU.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-D_TE6anL.js → FilePreviewPanel-xICJSzZ6.js} +2 -2
- package/dist/client/assets/{PreviewPanel-Dx5mB3w6.js → PreviewPanel-CK9d1fc5.js} +2 -2
- package/dist/client/assets/{angular-html-Z6GyEAg_.js → angular-html-BgzZxO9w.js} +2 -2
- package/dist/client/assets/{angular-ts-BdawRcND.js → angular-ts-YFEXVhe6.js} +2 -2
- package/dist/client/assets/{apl-0Nzwg3c9.js → apl-BoBwF9HI.js} +2 -2
- package/dist/client/assets/arrow-right-CXxWzaql.js +2 -0
- package/dist/client/assets/{astro-iJpYXO_m.js → astro-Ds5TXooD.js} +2 -2
- package/dist/client/assets/{blade-BeMtIYzp.js → blade-CDtJf5YR.js} +2 -2
- package/dist/client/assets/{c-n5Rj77uj.js → c-JH0umSmP.js} +2 -2
- package/dist/client/assets/{cobol-CfsSyjzP.js → cobol-gMss6BA3.js} +2 -2
- package/dist/client/assets/{coffee-LZB1mvnz.js → coffee-DESPXYwo.js} +2 -2
- package/dist/client/assets/{cpp-C689OQY_.js → cpp-CZztA1XN.js} +2 -2
- package/dist/client/assets/{crystal-CC7u-hEs.js → crystal-CDWlJpLE.js} +2 -2
- package/dist/client/assets/{css-eVqGM3Yx.js → css-CNPTTtX1.js} +2 -2
- package/dist/client/assets/{edge-CvU3NO69.js → edge-DSlrkPAr.js} +2 -2
- package/dist/client/assets/{elixir-gblyDHJ2.js → elixir-DrLl_wv7.js} +2 -2
- package/dist/client/assets/{elm-BEDREZ-r.js → elm-CwB92FlT.js} +2 -2
- package/dist/client/assets/{erb-C_pVTICR.js → erb-DXHrZ2ye.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-Z0wcn9Z2.js → fileCommentAnnotations-CuJ4hnrK.js} +2 -2
- package/dist/client/assets/{git-rebase-BvBzTqE6.js → git-rebase-uRnCoWg0.js} +2 -2
- package/dist/client/assets/{glimmer-js-y6xvI0dn.js → glimmer-js-DU9fMoZx.js} +2 -2
- package/dist/client/assets/{glimmer-ts-BrWleVTO.js → glimmer-ts-Bftqha5Z.js} +2 -2
- package/dist/client/assets/{glsl-Bkrdoinn.js → glsl-CVVphTeN.js} +2 -2
- package/dist/client/assets/{graphql-CSx1KLSt.js → graphql-CwED2DHh.js} +2 -2
- package/dist/client/assets/{hack-CEPENt6B.js → hack-CwiQSrzl.js} +2 -2
- package/dist/client/assets/{haml-CR4MMWD-.js → haml-C2iiqAZE.js} +2 -2
- package/dist/client/assets/{handlebars-BvZ81eM2.js → handlebars-BtvKqvl0.js} +2 -2
- package/dist/client/assets/{html-CJJ_Yk-p.js → html-BRvZZmrv.js} +2 -2
- package/dist/client/assets/{html-derivative-DyGTUpT_.js → html-derivative-rVjUDzCo.js} +2 -2
- package/dist/client/assets/{http-CthnayZT.js → http-Cvb4plLT.js} +2 -2
- package/dist/client/assets/{hurl-CySpRHwn.js → hurl-TX6OEub-.js} +2 -2
- package/dist/client/assets/{index-CstNfQU9.css → index-CTDey6k4.css} +1 -1
- package/dist/client/assets/index-DJPm8QPz.js +664 -0
- package/dist/client/assets/{java-eLetcmnB.js → java-CudutWch.js} +2 -2
- package/dist/client/assets/{javascript-bze-DcnN.js → javascript-D7gB3c_x.js} +2 -2
- package/dist/client/assets/{jinja-BwBfXVzv.js → jinja-Ce0WqJfV.js} +2 -2
- package/dist/client/assets/{jison-BG-6HBeN.js → jison-2U6TlYgq.js} +2 -2
- package/dist/client/assets/{json-D04kql_u.js → json-oe5h_2FV.js} +2 -2
- package/dist/client/assets/{jsx-B3RhaC78.js → jsx-B0f4Z6sx.js} +2 -2
- package/dist/client/assets/{julia-CLYU2NBT.js → julia-By6UDEAG.js} +2 -2
- package/dist/client/assets/{just-CQGSktLF.js → just-YTIO1wok.js} +2 -2
- package/dist/client/assets/{latex-B2r18a0i.js → latex-B9HLUatB.js} +2 -2
- package/dist/client/assets/{liquid-DSPVRc0S.js → liquid-DajcsI9e.js} +2 -2
- package/dist/client/assets/{lua-CBxs-my9.js → lua-CINm0eWo.js} +2 -2
- package/dist/client/assets/{marko-FkXUABHT.js → marko-DXvL73SK.js} +2 -2
- package/dist/client/assets/{mdc-BW-qJ07Y.js → mdc-CYsP-lJY.js} +2 -2
- package/dist/client/assets/{nginx-r-F4ISTc.js → nginx-CPKmhI0k.js} +2 -2
- package/dist/client/assets/{nim-CqGONDki.js → nim-DodS9Uy_.js} +2 -2
- package/dist/client/assets/{perl-YqxSDkbB.js → perl-COac5jCW.js} +2 -2
- package/dist/client/assets/{php-o53m7ISl.js → php-DLugOmp_.js} +2 -2
- package/dist/client/assets/{pug-DvC1IxmZ.js → pug-ukoxsRP-.js} +2 -2
- package/dist/client/assets/{qml-B_UHgwrf.js → qml-DEoQyDDD.js} +2 -2
- package/dist/client/assets/{r-BslRtXqA.js → r-CePyxHEB.js} +2 -2
- package/dist/client/assets/{razor-DbFRY8EJ.js → razor-uhGlTCzi.js} +2 -2
- package/dist/client/assets/{regexp-LUjvkiV5.js → regexp-BYNm_E6v.js} +2 -2
- package/dist/client/assets/{rst-DjMRC4dl.js → rst-BMMyVK7q.js} +2 -2
- package/dist/client/assets/{ruby-D1SSXqCW.js → ruby-BBl_q7lV.js} +2 -2
- package/dist/client/assets/{sas-8pH9ZpYC.js → sas-q_pdbMhY.js} +2 -2
- package/dist/client/assets/{scss-CLXZYmiL.js → scss-Bf5qLSyj.js} +2 -2
- package/dist/client/assets/{shellscript-C3x4iGUN.js → shellscript-CnkO-FF6.js} +2 -2
- package/dist/client/assets/{shellsession-4iw-70AB.js → shellsession-CGTIDec0.js} +2 -2
- package/dist/client/assets/{soy-BmuiXqhy.js → soy-D_X4Ckow.js} +2 -2
- package/dist/client/assets/{sql-CYtr1JGt.js → sql-wOawmdfM.js} +2 -2
- package/dist/client/assets/{stata-D-p-ifu4.js → stata-CKQN4lyV.js} +2 -2
- package/dist/client/assets/{surrealql-BQtaSVD9.js → surrealql-tjpKGjMP.js} +2 -2
- package/dist/client/assets/{svelte-BZ0IuCtg.js → svelte-mCzdghlf.js} +2 -2
- package/dist/client/assets/{templ-D_0C9uOG.js → templ-COV4wNUr.js} +2 -2
- package/dist/client/assets/{tex-Da564oMl.js → tex-CC1f6JM7.js} +2 -2
- package/dist/client/assets/textarea-BDADSCjn.js +46 -0
- package/dist/client/assets/{ts-tags-CCvt3g0S.js → ts-tags-CrIJ77ir.js} +2 -2
- package/dist/client/assets/{tsx-C1pKkLrg.js → tsx-IX8SGkp3.js} +2 -2
- package/dist/client/assets/{twig-ulf4RPD9.js → twig-DXR_a9UJ.js} +2 -2
- package/dist/client/assets/{typescript-Cm2BDYNJ.js → typescript-BgLtzzgs.js} +2 -2
- package/dist/client/assets/{vue-D38YVq_o.js → vue-Dzfv___3.js} +2 -2
- package/dist/client/assets/{vue-html-Dw9Iwm-j.js → vue-html-BHyzHcFt.js} +2 -2
- package/dist/client/assets/{vue-vine-DGhLcVZx.js → vue-vine-DOSBq7Kw.js} +2 -2
- package/dist/client/assets/{xml-Bm9IDqQ-.js → xml-BI5bRQfx.js} +2 -2
- package/dist/client/assets/{xsl-CLGFKBCX.js → xsl-B4FzWFML.js} +2 -2
- package/dist/client/assets/{yaml-C5Olrwb1.js → yaml-CHgKneKM.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-CTDRPB7h.js +0 -664
- 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.8";
|
|
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
|
|
@@ -7233,10 +7077,18 @@ const AssigneeField = TrimmedNonEmptyString.check(Schema$1.isMaxLength(TASK_ASSI
|
|
|
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." }));
|
|
7235
7079
|
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." }));
|
|
7080
|
+
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
7081
|
const Task = Schema$1.Struct({
|
|
7238
7082
|
taskId: TaskId,
|
|
7239
7083
|
/**
|
|
7084
|
+
* The human-readable id, e.g. `T-1` or `P4-12`: a per-prefix sequence
|
|
7085
|
+
* allocated by the store that owns the row, never reused, with gaps where
|
|
7086
|
+
* tasks were deleted. Nullable only for version skew - a row read from a
|
|
7087
|
+
* store that has not run the backfill migration decodes to `null` rather
|
|
7088
|
+
* than failing the whole board.
|
|
7089
|
+
*/
|
|
7090
|
+
readableId: Schema$1.NullOr(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
|
|
7091
|
+
/**
|
|
7240
7092
|
* The project this task belongs to, or `null` for a task that is not scoped
|
|
7241
7093
|
* to one. This references the existing workspace project registry rather than
|
|
7242
7094
|
* a task-board-private notion of a project.
|
|
@@ -7277,6 +7129,21 @@ const TaskCreateInput = Schema$1.Struct({
|
|
|
7277
7129
|
labels: Schema$1.optional(LabelsField)
|
|
7278
7130
|
});
|
|
7279
7131
|
/**
|
|
7132
|
+
* What an environment server sends the hub to file a task.
|
|
7133
|
+
*
|
|
7134
|
+
* Extends the public create input with the readable-id prefix, because the two
|
|
7135
|
+
* halves of the answer live on different machines: the prefix comes from the
|
|
7136
|
+
* project's checked-in `p4.json`, which only the submitting server can read,
|
|
7137
|
+
* while the sequence number must be allocated by the hub, which is the only
|
|
7138
|
+
* party that sees every machine's writes. Deliberately not part of
|
|
7139
|
+
* `TaskCreateInput` itself so an agent filing over MCP cannot pick a prefix -
|
|
7140
|
+
* the prefix belongs to the project, not to the caller.
|
|
7141
|
+
*/
|
|
7142
|
+
const HubTaskCreateInput = Schema$1.Struct({
|
|
7143
|
+
...TaskCreateInput.fields,
|
|
7144
|
+
readableIdPrefix: Schema$1.optional(TrimmedNonEmptyString)
|
|
7145
|
+
});
|
|
7146
|
+
/**
|
|
7280
7147
|
* Every field is optional and an omitted field is left untouched. The nullable
|
|
7281
7148
|
* fields accept an explicit `null` to clear them, which is why they are
|
|
7282
7149
|
* `NullOr` inside `optional` rather than merely optional.
|
|
@@ -7480,6 +7347,176 @@ const TaskToolError = Schema$1.Union([
|
|
|
7480
7347
|
TaskStoreError
|
|
7481
7348
|
]);
|
|
7482
7349
|
//#endregion
|
|
7350
|
+
//#region ../../packages/contracts/src/p4ProjectFile.ts
|
|
7351
|
+
/** File name of the checked-in P4 project file, resolved at the workspace root. */
|
|
7352
|
+
const P4_PROJECT_FILE_NAME = "p4.json";
|
|
7353
|
+
/** Public URL of the published JSON Schema for {@link P4ProjectFile}. */
|
|
7354
|
+
const P4_PROJECT_FILE_SCHEMA_URL = "https://p4.codes/schema/p4.json";
|
|
7355
|
+
const P4_PROJECT_FILE_PATH_MAX_LENGTH = 512;
|
|
7356
|
+
const P4_PROJECT_FILE_MAX_SCRIPTS = 50;
|
|
7357
|
+
const trimmedNonEmpty = (annotations, maxLength) => {
|
|
7358
|
+
const annotated = Schema$1.String.annotate(annotations);
|
|
7359
|
+
const encoded = maxLength === void 0 ? annotated.check(Schema$1.isNonEmpty()) : annotated.check(Schema$1.isNonEmpty(), Schema$1.isMaxLength(maxLength));
|
|
7360
|
+
return encoded.pipe(Schema$1.decodeTo(encoded, SchemaTransformation.trim()));
|
|
7361
|
+
};
|
|
7362
|
+
const P4ProjectFileScript = Schema$1.Struct({
|
|
7363
|
+
name: trimmedNonEmpty({ description: "Display name for the script, shown in the P4Code scripts menu." }),
|
|
7364
|
+
command: trimmedNonEmpty({ description: "Shell command executed in a P4Code terminal at the project root." }),
|
|
7365
|
+
icon: Schema$1.optionalKey(ProjectScriptIcon.annotate({ description: "Icon shown next to the script in the scripts menu. Defaults to \"play\"." })),
|
|
7366
|
+
runOnWorktreeCreate: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, the script runs automatically after a worktree is created for a new thread." })),
|
|
7367
|
+
previewUrl: Schema$1.optionalKey(trimmedNonEmpty({ description: "URL opened in the in-app browser preview when this script runs. Only honored on the desktop build." })),
|
|
7368
|
+
autoOpenPreview: Schema$1.optionalKey(Schema$1.Boolean.annotate({ description: "When true, automatically open the preview panel at `previewUrl` the moment the script starts." }))
|
|
7369
|
+
}).annotate({ description: "A project script that team members can import into P4Code." });
|
|
7370
|
+
const P4ProjectFile = Schema$1.Struct({
|
|
7371
|
+
$schema: Schema$1.optionalKey(Schema$1.String.annotate({ description: `URL of the JSON Schema for this file, typically "${P4_PROJECT_FILE_SCHEMA_URL}".` })),
|
|
7372
|
+
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)),
|
|
7373
|
+
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))),
|
|
7374
|
+
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)))
|
|
7375
|
+
}).annotate({
|
|
7376
|
+
title: "P4 project file",
|
|
7377
|
+
description: "Checked-in project configuration for P4Code (p4.json at the repository root). See https://p4.codes for documentation."
|
|
7378
|
+
});
|
|
7379
|
+
//#endregion
|
|
7380
|
+
//#region ../../packages/contracts/src/project.ts
|
|
7381
|
+
const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200;
|
|
7382
|
+
const PROJECT_WRITE_FILE_PATH_MAX_LENGTH = 512;
|
|
7383
|
+
const PROJECT_READ_FILE_PATH_MAX_LENGTH = 512;
|
|
7384
|
+
const ProjectSearchEntriesInput = Schema$1.Struct({
|
|
7385
|
+
cwd: TrimmedNonEmptyString,
|
|
7386
|
+
query: TrimmedNonEmptyString.check(Schema$1.isMaxLength(256)),
|
|
7387
|
+
limit: PositiveInt.check(Schema$1.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT))
|
|
7388
|
+
});
|
|
7389
|
+
const ProjectEntryKind = Schema$1.Literals(["file", "directory"]);
|
|
7390
|
+
const ProjectEntry = Schema$1.Struct({
|
|
7391
|
+
path: TrimmedNonEmptyString,
|
|
7392
|
+
kind: ProjectEntryKind
|
|
7393
|
+
});
|
|
7394
|
+
const ProjectSearchEntriesResult = Schema$1.Struct({
|
|
7395
|
+
entries: Schema$1.Array(ProjectEntry),
|
|
7396
|
+
truncated: Schema$1.Boolean
|
|
7397
|
+
});
|
|
7398
|
+
const ProjectListEntriesInput = Schema$1.Struct({ cwd: TrimmedNonEmptyString });
|
|
7399
|
+
const ProjectListEntriesResult = Schema$1.Struct({
|
|
7400
|
+
entries: Schema$1.Array(ProjectEntry),
|
|
7401
|
+
truncated: Schema$1.Boolean
|
|
7402
|
+
});
|
|
7403
|
+
const ProjectEntriesFailure = Schema$1.Literals([
|
|
7404
|
+
"workspace_root_not_found",
|
|
7405
|
+
"workspace_root_create_failed",
|
|
7406
|
+
"workspace_root_stat_failed",
|
|
7407
|
+
"workspace_root_not_directory",
|
|
7408
|
+
"search_index_create_failed",
|
|
7409
|
+
"search_index_scan_timed_out",
|
|
7410
|
+
"search_index_search_failed"
|
|
7411
|
+
]);
|
|
7412
|
+
function decodedProjectErrorMessage(props) {
|
|
7413
|
+
if (!("message" in props)) return void 0;
|
|
7414
|
+
return typeof props.message === "string" ? props.message : void 0;
|
|
7415
|
+
}
|
|
7416
|
+
var ProjectSearchEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectSearchEntriesError", {
|
|
7417
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7418
|
+
queryLength: Schema$1.optional(NonNegativeInt),
|
|
7419
|
+
limit: Schema$1.optional(PositiveInt),
|
|
7420
|
+
failure: Schema$1.optional(ProjectEntriesFailure),
|
|
7421
|
+
normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7422
|
+
timeout: Schema$1.optional(TrimmedNonEmptyString),
|
|
7423
|
+
detail: Schema$1.optional(TrimmedNonEmptyString),
|
|
7424
|
+
message: TrimmedNonEmptyString,
|
|
7425
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7426
|
+
}) {
|
|
7427
|
+
constructor(props) {
|
|
7428
|
+
super({
|
|
7429
|
+
...props,
|
|
7430
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to search workspace entries in '${props.cwd}'.`
|
|
7431
|
+
});
|
|
7432
|
+
}
|
|
7433
|
+
};
|
|
7434
|
+
var ProjectListEntriesError = class extends Schema$1.TaggedErrorClass()("ProjectListEntriesError", {
|
|
7435
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7436
|
+
failure: Schema$1.optional(ProjectEntriesFailure),
|
|
7437
|
+
normalizedCwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7438
|
+
timeout: Schema$1.optional(TrimmedNonEmptyString),
|
|
7439
|
+
detail: Schema$1.optional(TrimmedNonEmptyString),
|
|
7440
|
+
message: TrimmedNonEmptyString,
|
|
7441
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7442
|
+
}) {
|
|
7443
|
+
constructor(props) {
|
|
7444
|
+
super({
|
|
7445
|
+
...props,
|
|
7446
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to list workspace entries in '${props.cwd}'.`
|
|
7447
|
+
});
|
|
7448
|
+
}
|
|
7449
|
+
};
|
|
7450
|
+
const ProjectReadFileInput = Schema$1.Struct({
|
|
7451
|
+
cwd: TrimmedNonEmptyString,
|
|
7452
|
+
relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_READ_FILE_PATH_MAX_LENGTH))
|
|
7453
|
+
});
|
|
7454
|
+
const ProjectReadFileResult = Schema$1.Struct({
|
|
7455
|
+
relativePath: TrimmedNonEmptyString,
|
|
7456
|
+
contents: Schema$1.String,
|
|
7457
|
+
byteLength: NonNegativeInt,
|
|
7458
|
+
truncated: Schema$1.Boolean
|
|
7459
|
+
});
|
|
7460
|
+
const ProjectFileFailure = Schema$1.Literals([
|
|
7461
|
+
"workspace_path_outside_root",
|
|
7462
|
+
"resolved_path_outside_root",
|
|
7463
|
+
"path_not_file",
|
|
7464
|
+
"binary_file",
|
|
7465
|
+
"operation_failed"
|
|
7466
|
+
]);
|
|
7467
|
+
const ProjectFileOperation = Schema$1.Literals([
|
|
7468
|
+
"realpath-workspace-root",
|
|
7469
|
+
"realpath-target",
|
|
7470
|
+
"open",
|
|
7471
|
+
"stat",
|
|
7472
|
+
"read",
|
|
7473
|
+
"close",
|
|
7474
|
+
"make-directory",
|
|
7475
|
+
"write-file"
|
|
7476
|
+
]);
|
|
7477
|
+
var ProjectReadFileError = class extends Schema$1.TaggedErrorClass()("ProjectReadFileError", {
|
|
7478
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7479
|
+
relativePath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7480
|
+
failure: Schema$1.optional(ProjectFileFailure),
|
|
7481
|
+
resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7482
|
+
resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
|
|
7483
|
+
operation: Schema$1.optional(ProjectFileOperation),
|
|
7484
|
+
operationPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7485
|
+
message: TrimmedNonEmptyString,
|
|
7486
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7487
|
+
}) {
|
|
7488
|
+
constructor(props) {
|
|
7489
|
+
super({
|
|
7490
|
+
...props,
|
|
7491
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to read workspace file '${props.relativePath}' in '${props.cwd}'.`
|
|
7492
|
+
});
|
|
7493
|
+
}
|
|
7494
|
+
};
|
|
7495
|
+
const ProjectWriteFileInput = Schema$1.Struct({
|
|
7496
|
+
cwd: TrimmedNonEmptyString,
|
|
7497
|
+
relativePath: TrimmedNonEmptyString.check(Schema$1.isMaxLength(PROJECT_WRITE_FILE_PATH_MAX_LENGTH)),
|
|
7498
|
+
contents: Schema$1.String
|
|
7499
|
+
});
|
|
7500
|
+
const ProjectWriteFileResult = Schema$1.Struct({ relativePath: TrimmedNonEmptyString });
|
|
7501
|
+
var ProjectWriteFileError = class extends Schema$1.TaggedErrorClass()("ProjectWriteFileError", {
|
|
7502
|
+
cwd: Schema$1.optional(TrimmedNonEmptyString),
|
|
7503
|
+
relativePath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7504
|
+
failure: Schema$1.optional(ProjectFileFailure),
|
|
7505
|
+
resolvedPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7506
|
+
resolvedWorkspaceRoot: Schema$1.optional(TrimmedNonEmptyString),
|
|
7507
|
+
operation: Schema$1.optional(ProjectFileOperation),
|
|
7508
|
+
operationPath: Schema$1.optional(TrimmedNonEmptyString),
|
|
7509
|
+
message: TrimmedNonEmptyString,
|
|
7510
|
+
cause: Schema$1.optional(Schema$1.Defect())
|
|
7511
|
+
}) {
|
|
7512
|
+
constructor(props) {
|
|
7513
|
+
super({
|
|
7514
|
+
...props,
|
|
7515
|
+
message: decodedProjectErrorMessage(props) ?? `Failed to write workspace file '${props.relativePath}' in '${props.cwd}'.`
|
|
7516
|
+
});
|
|
7517
|
+
}
|
|
7518
|
+
};
|
|
7519
|
+
//#endregion
|
|
7483
7520
|
//#region ../../packages/contracts/src/agentAsset.ts
|
|
7484
7521
|
/**
|
|
7485
7522
|
* User-scope agent assets — the things that make an agent behave the same on
|
|
@@ -12030,6 +12067,51 @@ var _036_TaskRepositoryKey_default = Effect.gen(function* () {
|
|
|
12030
12067
|
`;
|
|
12031
12068
|
});
|
|
12032
12069
|
//#endregion
|
|
12070
|
+
//#region src/persistence/Migrations/037_TaskReadableIds.ts
|
|
12071
|
+
/**
|
|
12072
|
+
* Human-readable task ids (`T-1`, `P4-12`).
|
|
12073
|
+
*
|
|
12074
|
+
* `task_sequences` holds one row per prefix with the highest number ever
|
|
12075
|
+
* handed out. It only grows: deleting a task does not return its number, so a
|
|
12076
|
+
* readable id, once uttered, never comes to mean a different task.
|
|
12077
|
+
*
|
|
12078
|
+
* Existing rows are backfilled with the default `T` prefix in creation order.
|
|
12079
|
+
* Their projects' prefixes are deliberately not consulted — a prefix lives in
|
|
12080
|
+
* the project's checked-in `p4.json`, and a migration that shells out to read
|
|
12081
|
+
* workspaces (which may no longer be on disk) is exactly what a migration
|
|
12082
|
+
* should not be. New tasks pick up their project's prefix from creation on.
|
|
12083
|
+
*/
|
|
12084
|
+
var _037_TaskReadableIds_default = Effect.gen(function* () {
|
|
12085
|
+
const sql = yield* SqlClient.SqlClient;
|
|
12086
|
+
yield* sql`
|
|
12087
|
+
CREATE TABLE IF NOT EXISTS task_sequences (
|
|
12088
|
+
prefix TEXT PRIMARY KEY,
|
|
12089
|
+
last_seq INTEGER NOT NULL
|
|
12090
|
+
)
|
|
12091
|
+
`;
|
|
12092
|
+
yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
|
|
12093
|
+
yield* sql`
|
|
12094
|
+
UPDATE tasks
|
|
12095
|
+
SET readable_id = 'T-' || (
|
|
12096
|
+
SELECT COUNT(*)
|
|
12097
|
+
FROM tasks AS earlier
|
|
12098
|
+
WHERE earlier.created_at < tasks.created_at
|
|
12099
|
+
OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
|
|
12100
|
+
)
|
|
12101
|
+
WHERE readable_id IS NULL
|
|
12102
|
+
`;
|
|
12103
|
+
yield* sql`
|
|
12104
|
+
INSERT INTO task_sequences (prefix, last_seq)
|
|
12105
|
+
SELECT 'T', COUNT(*) FROM tasks
|
|
12106
|
+
WHERE true
|
|
12107
|
+
ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
|
|
12108
|
+
`;
|
|
12109
|
+
yield* sql`
|
|
12110
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
|
|
12111
|
+
ON tasks(readable_id)
|
|
12112
|
+
`;
|
|
12113
|
+
});
|
|
12114
|
+
//#endregion
|
|
12033
12115
|
//#region src/persistence/Migrations.ts
|
|
12034
12116
|
/**
|
|
12035
12117
|
* MigrationsLive - Migration runner with inline loader
|
|
@@ -12230,6 +12312,11 @@ const migrationEntries = [
|
|
|
12230
12312
|
36,
|
|
12231
12313
|
"TaskRepositoryKey",
|
|
12232
12314
|
_036_TaskRepositoryKey_default
|
|
12315
|
+
],
|
|
12316
|
+
[
|
|
12317
|
+
37,
|
|
12318
|
+
"TaskReadableIds",
|
|
12319
|
+
_037_TaskReadableIds_default
|
|
12233
12320
|
]
|
|
12234
12321
|
];
|
|
12235
12322
|
const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -13853,6 +13940,7 @@ const TaskListQuery = Schema$1.Struct({
|
|
|
13853
13940
|
});
|
|
13854
13941
|
const TASK_COLUMNS = `
|
|
13855
13942
|
task_id AS "taskId",
|
|
13943
|
+
readable_id AS "readableId",
|
|
13856
13944
|
project_id AS "projectId",
|
|
13857
13945
|
repository_key AS "repositoryKey",
|
|
13858
13946
|
thread_id AS "threadId",
|
|
@@ -13874,6 +13962,7 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13874
13962
|
execute: (row) => sql`
|
|
13875
13963
|
INSERT INTO tasks (
|
|
13876
13964
|
task_id,
|
|
13965
|
+
readable_id,
|
|
13877
13966
|
project_id,
|
|
13878
13967
|
repository_key,
|
|
13879
13968
|
thread_id,
|
|
@@ -13888,6 +13977,7 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13888
13977
|
)
|
|
13889
13978
|
VALUES (
|
|
13890
13979
|
${row.taskId},
|
|
13980
|
+
${row.readableId},
|
|
13891
13981
|
${row.projectId},
|
|
13892
13982
|
${row.repositoryKey},
|
|
13893
13983
|
${row.threadId},
|
|
@@ -13902,6 +13992,7 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13902
13992
|
)
|
|
13903
13993
|
ON CONFLICT (task_id)
|
|
13904
13994
|
DO UPDATE SET
|
|
13995
|
+
readable_id = excluded.readable_id,
|
|
13905
13996
|
project_id = excluded.project_id,
|
|
13906
13997
|
repository_key = excluded.repository_key,
|
|
13907
13998
|
thread_id = excluded.thread_id,
|
|
@@ -13920,6 +14011,26 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13920
14011
|
Result: TaskDbRow,
|
|
13921
14012
|
execute: ({ taskId }) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE task_id = ${taskId}`
|
|
13922
14013
|
});
|
|
14014
|
+
const getTaskRowByReadableId = SqlSchema.findOneOption({
|
|
14015
|
+
Request: Schema$1.String,
|
|
14016
|
+
Result: TaskDbRow,
|
|
14017
|
+
execute: (readableId) => sql`SELECT ${sql.literal(TASK_COLUMNS)} FROM tasks WHERE readable_id = ${readableId}`
|
|
14018
|
+
});
|
|
14019
|
+
/**
|
|
14020
|
+
* Take the next number for a prefix, creating the sequence at 1 on first
|
|
14021
|
+
* use. The row is never reset or decremented - a deleted task's number is
|
|
14022
|
+
* gone - which is what makes a readable id safe to say out loud.
|
|
14023
|
+
*/
|
|
14024
|
+
const allocateReadableIdSeq = SqlSchema.findAll({
|
|
14025
|
+
Request: Schema$1.Struct({ prefix: Schema$1.String }),
|
|
14026
|
+
Result: Schema$1.Struct({ lastSeq: Schema$1.Int }),
|
|
14027
|
+
execute: ({ prefix }) => sql`
|
|
14028
|
+
INSERT INTO task_sequences (prefix, last_seq)
|
|
14029
|
+
VALUES (${prefix}, 1)
|
|
14030
|
+
ON CONFLICT (prefix) DO UPDATE SET last_seq = last_seq + 1
|
|
14031
|
+
RETURNING last_seq AS "lastSeq"
|
|
14032
|
+
`
|
|
14033
|
+
});
|
|
13923
14034
|
const listTaskRows = SqlSchema.findAll({
|
|
13924
14035
|
Request: TaskListQuery,
|
|
13925
14036
|
Result: TaskDbRow,
|
|
@@ -13942,7 +14053,31 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
13942
14053
|
type: "upserted",
|
|
13943
14054
|
task: row
|
|
13944
14055
|
})));
|
|
14056
|
+
/**
|
|
14057
|
+
* The allocation and the insert share one transaction on purpose: a number
|
|
14058
|
+
* taken outside it would either be lost on a failed insert (a gap nobody
|
|
14059
|
+
* caused) or, worse, handed to two concurrent creates.
|
|
14060
|
+
*/
|
|
14061
|
+
const create = (row, readableIdPrefix) => sql.withTransaction(Effect.gen(function* () {
|
|
14062
|
+
const prefix = readableIdPrefix.toUpperCase();
|
|
14063
|
+
const seq = (yield* allocateReadableIdSeq({ prefix }))[0]?.lastSeq;
|
|
14064
|
+
if (seq === void 0) return yield* new PersistenceSqlError({
|
|
14065
|
+
operation: "TaskRepository.create:allocate",
|
|
14066
|
+
detail: "the sequence upsert returned no row",
|
|
14067
|
+
cause: null
|
|
14068
|
+
});
|
|
14069
|
+
const task = {
|
|
14070
|
+
...row,
|
|
14071
|
+
readableId: `${prefix}-${seq}`
|
|
14072
|
+
};
|
|
14073
|
+
yield* upsertTaskRow(task);
|
|
14074
|
+
return task;
|
|
14075
|
+
})).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.create:query")), Effect.tap((task) => publish({
|
|
14076
|
+
type: "upserted",
|
|
14077
|
+
task
|
|
14078
|
+
})));
|
|
13945
14079
|
const getById = (input) => getTaskRow(input).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getById:query")));
|
|
14080
|
+
const getByReadableId = (readableId) => getTaskRowByReadableId(readableId.toUpperCase()).pipe(Effect.mapError(toPersistenceSqlError("TaskRepository.getByReadableId:query")));
|
|
13946
14081
|
const list = (filter) => listTaskRows({
|
|
13947
14082
|
projectId: filter.projectId ?? null,
|
|
13948
14083
|
repositoryKey: filter.repositoryKey ?? null,
|
|
@@ -14017,10 +14152,12 @@ const makeTaskRepository = Effect.gen(function* () {
|
|
|
14017
14152
|
taskId: input.taskId
|
|
14018
14153
|
})));
|
|
14019
14154
|
return {
|
|
14155
|
+
create,
|
|
14020
14156
|
upsert,
|
|
14021
14157
|
patch,
|
|
14022
14158
|
patchIfStatus,
|
|
14023
14159
|
getById,
|
|
14160
|
+
getByReadableId,
|
|
14024
14161
|
list,
|
|
14025
14162
|
deleteById,
|
|
14026
14163
|
streamChanges: Stream.fromPubSub(changes),
|
|
@@ -14109,12 +14246,13 @@ const getTaskRoute = HttpRouter.add("GET", "/tasks/:taskId", respondToHubFailure
|
|
|
14109
14246
|
yield* authenticateHubRequest();
|
|
14110
14247
|
const params = yield* HttpRouter.params;
|
|
14111
14248
|
const taskId = TaskId.make(params.taskId ?? "");
|
|
14112
|
-
const
|
|
14249
|
+
const tasks = yield* TaskRepository;
|
|
14250
|
+
const found = isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
|
|
14113
14251
|
return Option.isNone(found) ? notFound(taskId) : HttpServerResponse.jsonUnsafe(found.value);
|
|
14114
14252
|
})));
|
|
14115
14253
|
const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Effect.gen(function* () {
|
|
14116
14254
|
yield* authenticateHubRequest();
|
|
14117
|
-
const input = yield* decodeOrInvalid(
|
|
14255
|
+
const input = yield* decodeOrInvalid(HubTaskCreateInput, "Request body does not match HubTaskCreateInput.")(yield* readJsonBody());
|
|
14118
14256
|
const tasks = yield* TaskRepository;
|
|
14119
14257
|
const crypto = yield* Crypto.Crypto;
|
|
14120
14258
|
const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
@@ -14133,8 +14271,8 @@ const createTaskRoute = HttpRouter.add("POST", "/tasks", respondToHubFailures(Ef
|
|
|
14133
14271
|
createdAt: timestamp,
|
|
14134
14272
|
updatedAt: timestamp
|
|
14135
14273
|
};
|
|
14136
|
-
yield* tasks.
|
|
14137
|
-
return HttpServerResponse.jsonUnsafe(
|
|
14274
|
+
const task = yield* tasks.create(row, input.readableIdPrefix ?? "T");
|
|
14275
|
+
return HttpServerResponse.jsonUnsafe(task, { status: 201 });
|
|
14138
14276
|
})));
|
|
14139
14277
|
const updateTaskRoute = HttpRouter.add("PATCH", "/tasks/:taskId", respondToHubFailures(Effect.gen(function* () {
|
|
14140
14278
|
yield* authenticateHubRequest();
|
|
@@ -14372,6 +14510,50 @@ var _003_AgentAssets_default = Effect.gen(function* () {
|
|
|
14372
14510
|
`;
|
|
14373
14511
|
});
|
|
14374
14512
|
//#endregion
|
|
14513
|
+
//#region src/hub/Migrations/004_TaskReadableIds.ts
|
|
14514
|
+
/**
|
|
14515
|
+
* Human-readable task ids (`T-1`, `P4-12`).
|
|
14516
|
+
*
|
|
14517
|
+
* `task_sequences` holds one row per prefix with the highest number ever
|
|
14518
|
+
* handed out. It only grows: deleting a task does not return its number, so a
|
|
14519
|
+
* readable id, once uttered, never comes to mean a different task.
|
|
14520
|
+
*
|
|
14521
|
+
* Existing rows are backfilled with the default `T` prefix in creation order.
|
|
14522
|
+
* Prefixes cannot be consulted here at all: they live in each project's
|
|
14523
|
+
* checked-in `p4.json`, and the hub has no checkouts. New tasks arrive with a
|
|
14524
|
+
* prefix resolved by the submitting environment server.
|
|
14525
|
+
*/
|
|
14526
|
+
var _004_TaskReadableIds_default = Effect.gen(function* () {
|
|
14527
|
+
const sql = yield* SqlClient.SqlClient;
|
|
14528
|
+
yield* sql`
|
|
14529
|
+
CREATE TABLE IF NOT EXISTS task_sequences (
|
|
14530
|
+
prefix TEXT PRIMARY KEY,
|
|
14531
|
+
last_seq INTEGER NOT NULL
|
|
14532
|
+
)
|
|
14533
|
+
`;
|
|
14534
|
+
yield* sql`ALTER TABLE tasks ADD COLUMN readable_id TEXT`;
|
|
14535
|
+
yield* sql`
|
|
14536
|
+
UPDATE tasks
|
|
14537
|
+
SET readable_id = 'T-' || (
|
|
14538
|
+
SELECT COUNT(*)
|
|
14539
|
+
FROM tasks AS earlier
|
|
14540
|
+
WHERE earlier.created_at < tasks.created_at
|
|
14541
|
+
OR (earlier.created_at = tasks.created_at AND earlier.task_id <= tasks.task_id)
|
|
14542
|
+
)
|
|
14543
|
+
WHERE readable_id IS NULL
|
|
14544
|
+
`;
|
|
14545
|
+
yield* sql`
|
|
14546
|
+
INSERT INTO task_sequences (prefix, last_seq)
|
|
14547
|
+
SELECT 'T', COUNT(*) FROM tasks
|
|
14548
|
+
WHERE true
|
|
14549
|
+
ON CONFLICT (prefix) DO UPDATE SET last_seq = MAX(last_seq, excluded.last_seq)
|
|
14550
|
+
`;
|
|
14551
|
+
yield* sql`
|
|
14552
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_readable_id
|
|
14553
|
+
ON tasks(readable_id)
|
|
14554
|
+
`;
|
|
14555
|
+
});
|
|
14556
|
+
//#endregion
|
|
14375
14557
|
//#region src/hub/Migrations.ts
|
|
14376
14558
|
/**
|
|
14377
14559
|
* Hub migrations.
|
|
@@ -14397,6 +14579,11 @@ const hubMigrationEntries = [
|
|
|
14397
14579
|
3,
|
|
14398
14580
|
"AgentAssets",
|
|
14399
14581
|
_003_AgentAssets_default
|
|
14582
|
+
],
|
|
14583
|
+
[
|
|
14584
|
+
4,
|
|
14585
|
+
"TaskReadableIds",
|
|
14586
|
+
_004_TaskReadableIds_default
|
|
14400
14587
|
]
|
|
14401
14588
|
];
|
|
14402
14589
|
const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -32038,6 +32225,8 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
|
|
|
32038
32225
|
* presentation and is not.
|
|
32039
32226
|
*
|
|
32040
32227
|
* The id and timestamp are passed in so this stays pure and holds no clock.
|
|
32228
|
+
* The readable id is absent on purpose: the store allocates it at insert time,
|
|
32229
|
+
* inside the same transaction as the sequence bump.
|
|
32041
32230
|
*/
|
|
32042
32231
|
const buildTaskRow = (input, taskId, timestamp, repositoryKey = null) => ({
|
|
32043
32232
|
taskId,
|
|
@@ -32073,6 +32262,23 @@ const resolveTaskRepositoryKey = Effect.fn("taskBoard.resolveRepositoryKey")(fun
|
|
|
32073
32262
|
return (yield* (yield* RepositoryIdentityResolver).resolve(project.value.workspaceRoot))?.canonicalKey ?? null;
|
|
32074
32263
|
});
|
|
32075
32264
|
/**
|
|
32265
|
+
* Resolve the readable-id prefix for a task's project.
|
|
32266
|
+
*
|
|
32267
|
+
* The prefix lives in the project's checked-in `p4.json` (`taskPrefix`), not in
|
|
32268
|
+
* this server's database: it is a property of the repository, agreed on by
|
|
32269
|
+
* everyone who clones it, exactly like the scripts beside it. A task with no
|
|
32270
|
+
* project, a removed project, or a `p4.json` without the field all fall back to
|
|
32271
|
+
* the default `T` - filing the task always beats refusing it over a label.
|
|
32272
|
+
*/
|
|
32273
|
+
const resolveTaskReadableIdPrefix = Effect.fn("taskBoard.resolveReadableIdPrefix")(function* (projectId) {
|
|
32274
|
+
if (projectId == null) return "T";
|
|
32275
|
+
const project = yield* (yield* ProjectionProjectRepository).getById({ projectId }).pipe(Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
32276
|
+
if (Option.isNone(project)) return "T";
|
|
32277
|
+
const file = yield* (yield* P4ProjectFileLoader).load(project.value.workspaceRoot);
|
|
32278
|
+
if (Option.isNone(file) || file.value.taskPrefix === void 0) return "T";
|
|
32279
|
+
return file.value.taskPrefix.toUpperCase();
|
|
32280
|
+
});
|
|
32281
|
+
/**
|
|
32076
32282
|
* Everything that decides whether a task can be started, as one pure function.
|
|
32077
32283
|
*
|
|
32078
32284
|
* Kept out of the RPC handler because the three refusals are the whole of the
|
|
@@ -42697,9 +42903,12 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
|
|
|
42697
42903
|
[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
42904
|
[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
42905
|
[WS_METHODS.tasksCreate]: (input) => observeRpcEffect$1(WS_METHODS.tasksCreate, Effect.gen(function* () {
|
|
42700
|
-
const
|
|
42701
|
-
yield*
|
|
42702
|
-
|
|
42906
|
+
const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
42907
|
+
const timestamp = yield* nowIso$5;
|
|
42908
|
+
const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
|
|
42909
|
+
const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
|
|
42910
|
+
const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
|
|
42911
|
+
return { task: yield* taskRepository.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("tasks.create"))) };
|
|
42703
42912
|
}), { "rpc.aggregate": "tasks" }),
|
|
42704
42913
|
[WS_METHODS.tasksUpdate]: (input) => observeRpcEffect$1(WS_METHODS.tasksUpdate, Effect.gen(function* () {
|
|
42705
42914
|
const timestamp = yield* nowIso$5;
|
|
@@ -43010,12 +43219,13 @@ const websocketRpcRouteLayer = Layer.unwrap(Effect.gen(function* () {
|
|
|
43010
43219
|
const serverSelfUpdate = yield* ServerSelfUpdate;
|
|
43011
43220
|
const taskRepository = yield* TaskRepository;
|
|
43012
43221
|
const projectionProjects = yield* ProjectionProjectRepository;
|
|
43222
|
+
const p4ProjectFileLoader = yield* P4ProjectFileLoader;
|
|
43013
43223
|
return HttpRouter.add("GET", "/ws", Effect.gen(function* () {
|
|
43014
43224
|
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
43015
43225
|
const serverAuth = yield* EnvironmentAuth;
|
|
43016
43226
|
const sessions = yield* SessionStore;
|
|
43017
43227
|
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))))));
|
|
43228
|
+
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
43229
|
return yield* Effect.acquireUseRelease(sessions.markConnected(session.sessionId), () => rpcWebSocketHttpEffect, () => sessions.markDisconnected(session.sessionId));
|
|
43020
43230
|
}).pipe(Effect.catchTags({
|
|
43021
43231
|
EnvironmentAuthInvalidError: HttpServerRespondable.toResponse,
|
|
@@ -43120,17 +43330,16 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
|
|
|
43120
43330
|
return Option.some(yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable("getById", "the response did not match the contract"))));
|
|
43121
43331
|
});
|
|
43122
43332
|
/**
|
|
43123
|
-
*
|
|
43124
|
-
* store
|
|
43125
|
-
*
|
|
43126
|
-
*
|
|
43127
|
-
*
|
|
43128
|
-
*
|
|
43129
|
-
* that quietly would let two servers mint colliding ids.
|
|
43333
|
+
* Both writes here POST the hub's create route, and that is a real
|
|
43334
|
+
* difference from the local store: the hub mints its own ids, timestamps and
|
|
43335
|
+
* readable id, so a row written through this comes back with the hub's
|
|
43336
|
+
* version of all three. The caller's `taskId` is not carried - honouring it
|
|
43337
|
+
* would need a hub route that takes one, and inventing that quietly would
|
|
43338
|
+
* let two servers mint colliding ids.
|
|
43130
43339
|
*/
|
|
43131
|
-
const
|
|
43132
|
-
const settings = yield* requireSettings(
|
|
43133
|
-
const response = yield* send(
|
|
43340
|
+
const postCreate = (operation, row, readableIdPrefix) => Effect.gen(function* () {
|
|
43341
|
+
const settings = yield* requireSettings(operation);
|
|
43342
|
+
const response = yield* send(operation, settings, HttpClientRequest.bodyJsonUnsafe(HttpClientRequest.post(`${settings.baseUrl}/tasks`), {
|
|
43134
43343
|
...row.projectId === null ? {} : { projectId: row.projectId },
|
|
43135
43344
|
...row.repositoryKey === null ? {} : { repositoryKey: row.repositoryKey },
|
|
43136
43345
|
...row.threadId === null ? {} : { threadId: row.threadId },
|
|
@@ -43139,15 +43348,24 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
|
|
|
43139
43348
|
status: row.status,
|
|
43140
43349
|
priority: row.priority,
|
|
43141
43350
|
...row.assignee === null ? {} : { assignee: row.assignee },
|
|
43142
|
-
labels: row.labels
|
|
43351
|
+
labels: row.labels,
|
|
43352
|
+
...readableIdPrefix === void 0 ? {} : { readableIdPrefix }
|
|
43143
43353
|
}));
|
|
43144
|
-
if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable(
|
|
43145
|
-
const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(
|
|
43354
|
+
if (response.status !== 201 && response.status !== 200) return yield* hubUnavailable(operation, `status ${response.status}`);
|
|
43355
|
+
const created = yield* response.json.pipe(Effect.flatMap(decodeTask), Effect.mapError(() => hubUnavailable(operation, "the response did not match the contract")));
|
|
43146
43356
|
yield* publish({
|
|
43147
43357
|
type: "upserted",
|
|
43148
43358
|
task: created
|
|
43149
43359
|
});
|
|
43360
|
+
return created;
|
|
43150
43361
|
});
|
|
43362
|
+
const create = (row, readableIdPrefix) => postCreate("create", row, readableIdPrefix);
|
|
43363
|
+
const upsert = (row) => postCreate("upsert", row).pipe(Effect.asVoid);
|
|
43364
|
+
/**
|
|
43365
|
+
* Served by the same hub route as `getById`: readable ids and UUIDs are
|
|
43366
|
+
* disjoint by shape, so the hub dispatches on what it is handed.
|
|
43367
|
+
*/
|
|
43368
|
+
const getByReadableId = (readableId) => getById({ taskId: TaskId.make(readableId) });
|
|
43151
43369
|
const patch = (input) => Effect.gen(function* () {
|
|
43152
43370
|
const settings = yield* requireSettings("patch");
|
|
43153
43371
|
const { taskId, ...fields } = input;
|
|
@@ -43222,10 +43440,12 @@ const makeRemoteTaskRepository = Effect.fn("makeRemoteTaskRepository")(function*
|
|
|
43222
43440
|
}).pipe(Effect.ignoreCause({ log: true }));
|
|
43223
43441
|
yield* Effect.forever(pollOnce.pipe(Effect.andThen(Effect.sleep(POLL_INTERVAL)))).pipe(Effect.forkScoped);
|
|
43224
43442
|
return {
|
|
43443
|
+
create,
|
|
43225
43444
|
upsert,
|
|
43226
43445
|
patch,
|
|
43227
43446
|
patchIfStatus,
|
|
43228
43447
|
getById,
|
|
43448
|
+
getByReadableId,
|
|
43229
43449
|
list,
|
|
43230
43450
|
deleteById,
|
|
43231
43451
|
streamChanges: Stream.fromPubSub(changes),
|
|
@@ -43275,10 +43495,12 @@ const makeRoutedTaskRepository = Effect.gen(function* () {
|
|
|
43275
43495
|
*/
|
|
43276
43496
|
const pick = usingHub.pipe(Effect.map((hub) => hub ? remote : local));
|
|
43277
43497
|
return {
|
|
43498
|
+
create: (row, readableIdPrefix) => pick.pipe(Effect.flatMap((repository) => repository.create(row, readableIdPrefix))),
|
|
43278
43499
|
upsert: (row) => pick.pipe(Effect.flatMap((repository) => repository.upsert(row))),
|
|
43279
43500
|
patch: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patch(input, updatedAt))),
|
|
43280
43501
|
patchIfStatus: (input, updatedAt) => pick.pipe(Effect.flatMap((repository) => repository.patchIfStatus(input, updatedAt))),
|
|
43281
43502
|
getById: (input) => pick.pipe(Effect.flatMap((repository) => repository.getById(input))),
|
|
43503
|
+
getByReadableId: (readableId) => pick.pipe(Effect.flatMap((repository) => repository.getByReadableId(readableId))),
|
|
43282
43504
|
list: (filter) => pick.pipe(Effect.flatMap((repository) => repository.list(filter))),
|
|
43283
43505
|
deleteById: (input) => pick.pipe(Effect.flatMap((repository) => repository.deleteById(input))),
|
|
43284
43506
|
get streamChanges() {
|
|
@@ -81083,8 +81305,15 @@ const writeDependencies = [
|
|
|
81083
81305
|
ProjectionProjectRepository,
|
|
81084
81306
|
RepositoryIdentityResolver
|
|
81085
81307
|
];
|
|
81086
|
-
/**
|
|
81087
|
-
|
|
81308
|
+
/**
|
|
81309
|
+
* `task_create` also mints the task id (crypto) and resolves the readable-id
|
|
81310
|
+
* prefix from the project's checked-in `p4.json` (the file loader).
|
|
81311
|
+
*/
|
|
81312
|
+
const createDependencies = [
|
|
81313
|
+
...writeDependencies,
|
|
81314
|
+
Crypto.Crypto,
|
|
81315
|
+
P4ProjectFileLoader
|
|
81316
|
+
];
|
|
81088
81317
|
const TaskListResult = Schema$1.Struct({ tasks: Schema$1.Array(Task) });
|
|
81089
81318
|
const TaskListTool = Tool.make("task_list", {
|
|
81090
81319
|
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 +81323,7 @@ const TaskListTool = Tool.make("task_list", {
|
|
|
81094
81323
|
dependencies
|
|
81095
81324
|
}).annotate(Tool.Title, "List tasks").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
|
|
81096
81325
|
const TaskGetTool = Tool.make("task_get", {
|
|
81097
|
-
description: "Read a single task by id, including its body, labels and linked thread.",
|
|
81326
|
+
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
81327
|
parameters: TaskGetInput,
|
|
81099
81328
|
success: Task,
|
|
81100
81329
|
failure: TaskToolError,
|
|
@@ -81144,6 +81373,15 @@ const TaskToolkit = Toolkit.make(TaskListTool, TaskGetTool, TaskCurrentTool, Tas
|
|
|
81144
81373
|
//#endregion
|
|
81145
81374
|
//#region src/mcp/toolkits/tasks/handlers.ts
|
|
81146
81375
|
const nowIso$1 = Effect.map(DateTime.now, DateTime.formatIso);
|
|
81376
|
+
/**
|
|
81377
|
+
* Agents reference tasks by whichever id they have seen, so any `taskId`
|
|
81378
|
+
* parameter accepts the readable id (`P4-12`) as well as the UUID. The two are
|
|
81379
|
+
* disjoint by shape, which is what makes the dispatch safe.
|
|
81380
|
+
*/
|
|
81381
|
+
const findTask = (taskId) => Effect.gen(function* () {
|
|
81382
|
+
const tasks = yield* TaskRepository;
|
|
81383
|
+
return isReadableTaskId(taskId) ? yield* tasks.getByReadableId(taskId) : yield* tasks.getById({ taskId });
|
|
81384
|
+
});
|
|
81147
81385
|
const TaskToolkitHandlersLive = TaskToolkit.toLayer({
|
|
81148
81386
|
task_list: (input) => Effect.gen(function* () {
|
|
81149
81387
|
yield* requireTaskCapability();
|
|
@@ -81151,7 +81389,7 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
|
|
|
81151
81389
|
}),
|
|
81152
81390
|
task_get: (input) => Effect.gen(function* () {
|
|
81153
81391
|
yield* requireTaskCapability();
|
|
81154
|
-
const found = yield* (
|
|
81392
|
+
const found = yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_get")));
|
|
81155
81393
|
if (Option.isNone(found)) return yield* new TaskNotFoundError({ taskId: input.taskId });
|
|
81156
81394
|
return found.value;
|
|
81157
81395
|
}),
|
|
@@ -81167,16 +81405,25 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
|
|
|
81167
81405
|
yield* requireTaskCapability();
|
|
81168
81406
|
const tasks = yield* TaskRepository;
|
|
81169
81407
|
const crypto = yield* Crypto.Crypto;
|
|
81170
|
-
const
|
|
81171
|
-
yield*
|
|
81172
|
-
|
|
81408
|
+
const taskId = TaskId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
81409
|
+
const timestamp = yield* nowIso$1;
|
|
81410
|
+
const repositoryKey = yield* resolveTaskRepositoryKey(input.projectId);
|
|
81411
|
+
const readableIdPrefix = yield* resolveTaskReadableIdPrefix(input.projectId);
|
|
81412
|
+
const row = buildTaskRow(input, taskId, timestamp, repositoryKey);
|
|
81413
|
+
return yield* tasks.create(row, readableIdPrefix).pipe(Effect.catch(toTaskStoreError("task_create")));
|
|
81173
81414
|
}),
|
|
81174
81415
|
task_update: (input) => Effect.gen(function* () {
|
|
81175
81416
|
yield* requireTaskCapability();
|
|
81176
81417
|
const tasks = yield* TaskRepository;
|
|
81177
81418
|
const timestamp = yield* nowIso$1;
|
|
81178
|
-
const
|
|
81419
|
+
const taskId = isReadableTaskId(input.taskId) ? Option.getOrNull(yield* findTask(input.taskId).pipe(Effect.catch(toTaskStoreError("task_update"))))?.taskId : input.taskId;
|
|
81420
|
+
if (taskId === void 0) return yield* new TaskNotFoundError({ taskId: input.taskId });
|
|
81421
|
+
const patch = input.projectId === void 0 ? {
|
|
81422
|
+
...input,
|
|
81423
|
+
taskId
|
|
81424
|
+
} : {
|
|
81179
81425
|
...input,
|
|
81426
|
+
taskId,
|
|
81180
81427
|
repositoryKey: yield* resolveTaskRepositoryKey(input.projectId)
|
|
81181
81428
|
};
|
|
81182
81429
|
const patched = yield* tasks.patch(patch, timestamp).pipe(Effect.catch(toTaskStoreError("task_update")));
|
|
@@ -84890,7 +85137,7 @@ const RuntimeDependenciesLive = ReactorLayerLive.pipe(Layer.provideMerge(Checkpo
|
|
|
84890
85137
|
*/
|
|
84891
85138
|
const AssetSyncLive = layer$54.pipe(Layer.provideMerge(layer$55), Layer.provideMerge(layer$56));
|
|
84892
85139
|
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));
|
|
85140
|
+
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
85141
|
const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
|
|
84895
85142
|
const config = yield* ServerConfig$1;
|
|
84896
85143
|
yield* fixPath();
|