Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
80e9fd0
Replace package id with name; add package APIs
chrisgregan Jun 12, 2026
217e882
Add package tools, history and install/publish
chrisgregan Jun 13, 2026
48c3857
Add package delete/unpublish/status features
chrisgregan Jun 14, 2026
33c895f
Create PackageVersionResolver.cs
chrisgregan Jun 14, 2026
50b3b44
Add HISTORY.md metadata, parse refs, and stale checks
chrisgregan Jun 14, 2026
eba7baf
Add pages subsystem and Author setting
chrisgregan Jun 14, 2026
b486d73
Add scheduled automated dialog answers
chrisgregan Jun 15, 2026
2759ff4
Support auto-answer for dialogs via messenger
chrisgregan Jun 15, 2026
d00a9a4
Add integration publish/install/unpublish tests
chrisgregan Jun 15, 2026
2fd8742
Add rescan, hash option, and alias handling
chrisgregan Jun 15, 2026
1351c33
Update package_info.md
chrisgregan Jun 15, 2026
ac2d119
Exclude app_answer_dialog guide from Release builds
chrisgregan Jun 16, 2026
3d7da88
Bump esbuild and update tiptap bundle
chrisgregan Jun 16, 2026
23a4e23
Dialog automation: use DialogKind and related fixes
chrisgregan Jun 16, 2026
1a7b2ac
Store Workshop Key in editor settings
chrisgregan Jun 17, 2026
f0ff9b4
Merge branch 'main' into workshop-api
chrisgregan Jun 17, 2026
786f75c
Consolidate glob matching into GlobHelper
chrisgregan Jun 18, 2026
8d05e6d
Document lowercase SHA-256 hash, simplify docs
chrisgregan Jun 18, 2026
f09aa78
Rename PackageHistoryFile to PackageHistoryHelper
chrisgregan Jun 18, 2026
022e554
Clarify package API documentation wording
chrisgregan Jun 18, 2026
94b65dc
Add masked Secret Input dialog & workshop key flow
chrisgregan Jun 18, 2026
d999ec1
Add workshop connection check and UI feedback
chrisgregan Jun 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,6 @@ solution-config.props
**/Celbridge.Spreadsheet/Package/lib/
**/SpreadsheetLicenseKeys.private.cs

# Package API credentials
**/PackageApiCredentials.private.cs

# Celbridge ephemeral data
.cache/
.trash/
Expand Down
139 changes: 116 additions & 23 deletions Source/Celbridge/Resources/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -414,50 +414,95 @@
<data name="SettingsPage_WorkshopSection" xml:space="preserve">
<value>Workshop</value>
</data>
<data name="SettingsPage_WorkshopDescription" xml:space="preserve">
<value>Connect to a Workshop to publish and install packages and pages.</value>
</data>
<data name="SettingsPage_WorkshopUrl" xml:space="preserve">
<value>Workshop URL</value>
</data>
<data name="SettingsPage_ApplicationKey" xml:space="preserve">
<value>Application Key</value>
<data name="SettingsPage_WorkshopUrlTooltip" xml:space="preserve">
<value>The web address of your Workshop server, where packages and pages are published.</value>
</data>
<data name="SettingsPage_SaveConnection" xml:space="preserve">
<value>Save</value>
<data name="SettingsPage_WorkshopKey" xml:space="preserve">
<value>Workshop Key</value>
</data>
<data name="SettingsPage_ClearConnection" xml:space="preserve">
<value>Clear</value>
<data name="SettingsPage_WorkshopKeyTooltip" xml:space="preserve">
<value>The secret key issued by your Workshop. Keep it private; it authenticates your requests.</value>
</data>
<data name="SettingsPage_ReplaceKey" xml:space="preserve">
<value>Replace</value>
<data name="SettingsPage_Author" xml:space="preserve">
<value>Author Name</value>
</data>
<data name="SettingsPage_CancelReplaceKey" xml:space="preserve">
<value>Cancel</value>
<data name="SettingsPage_AuthorTooltip" xml:space="preserve">
<value>Your name, recorded with the packages and pages you publish to the workshop.</value>
</data>
<data name="SettingsPage_AuthorPlaceholder" xml:space="preserve">
<value>Your name</value>
</data>
<data name="SettingsPage_SetWorkshopKey" xml:space="preserve">
<value>Set Workshop Key</value>
</data>
<data name="SettingsPage_ChangeKey" xml:space="preserve">
<value>Change</value>
</data>
<data name="SettingsPage_RemoveKey" xml:space="preserve">
<value>Remove</value>
</data>
<data name="SettingsPage_SetWorkshopKeyDialogTitle" xml:space="preserve">
<value>Set Workshop Key</value>
</data>
<data name="SettingsPage_ChangeWorkshopKeyDialogTitle" xml:space="preserve">
<value>Change Workshop Key</value>
</data>
<data name="SettingsPage_SaveKeyButton" xml:space="preserve">
<value>Save</value>
</data>
<data name="SettingsPage_RemoveWorkshopKeyTitle" xml:space="preserve">
<value>Remove Workshop Key</value>
</data>
<data name="SettingsPage_RemoveWorkshopKeyMessage" xml:space="preserve">
<value>Remove the stored Workshop Key? You will need to enter it again to reconnect to the workshop.</value>
</data>
<data name="SettingsPage_CredentialStoreUnavailable" xml:space="preserve">
<value>Credential storage is not available on this platform. The Workshop connection cannot be configured.</value>
</data>
<data name="SettingsPage_StoredConnectionUnreadable" xml:space="preserve">
<value>The stored Workshop connection could not be read. Enter the Workshop URL and Application Key again, or clear the connection.</value>
<value>The stored Workshop connection could not be read. Enter the Workshop URL and Workshop Key again, or clear the stored key.</value>
</data>
<data name="SettingsPage_InvalidWorkshopUrl" xml:space="preserve">
<value>The Workshop URL must be an https:// address. http:// is only allowed for localhost.</value>
</data>
<data name="SettingsPage_EmptyApplicationKey" xml:space="preserve">
<value>Enter an Application Key.</value>
<data name="SettingsPage_EmptyWorkshopUrl" xml:space="preserve">
<value>Enter a valid Workshop URL.</value>
</data>
<data name="SettingsPage_EmptyWorkshopKey" xml:space="preserve">
<value>Enter a Workshop Key.</value>
</data>
<data name="SettingsPage_AuthorRequired" xml:space="preserve">
<value>Add an Author Name to publish packages and pages.</value>
</data>
<data name="SettingsPage_SaveConnectionFailed" xml:space="preserve">
<value>Failed to save the Workshop connection.</value>
</data>
<data name="SettingsPage_ClearConnectionFailed" xml:space="preserve">
<value>Failed to clear the Workshop connection.</value>
<data name="SettingsPage_RemoveWorkshopKeyFailed" xml:space="preserve">
<value>Failed to remove the Workshop Key.</value>
</data>
<data name="SettingsPage_ConnectionSaved" xml:space="preserve">
<value>Workshop connection saved.</value>
</data>
<data name="SettingsPage_ConnectionSavedPrefixWarning" xml:space="preserve">
<value>Workshop connection saved. The Application Key does not start with 'kpf_', so check that it was entered correctly.</value>
<data name="SettingsPage_WorkshopKeyRemoved" xml:space="preserve">
<value>Workshop Key removed. Set a new key to reconnect.</value>
</data>
<data name="SettingsPage_CheckingConnection" xml:space="preserve">
<value>Checking connection to the workshop...</value>
</data>
<data name="SettingsPage_ConnectionVerified" xml:space="preserve">
<value>Connected to the workshop.</value>
</data>
<data name="SettingsPage_ConnectionCleared" xml:space="preserve">
<value>Workshop connection cleared.</value>
<data name="SettingsPage_WorkshopKeyRejected" xml:space="preserve">
<value>The workshop rejected this key. Check that you copied the whole key and that it is still valid.</value>
</data>
<data name="SettingsPage_ConnectionUnverified" xml:space="preserve">
<value>Workshop Key saved, but the connection couldn't be verified right now.</value>
</data>
<data name="NewProjectDialog_CreateSubfolder" xml:space="preserve">
<value>Create directory for project</value>
Expand Down Expand Up @@ -639,7 +684,7 @@ Do you wish to continue?</value>
<value>Package Load Error</value>
</data>
<data name="ConsolePanel_PackageLoadErrorMessage" xml:space="preserve">
<value>One or more packages failed to load. See the log for details.</value>
<value>One or more packages failed to load. See the project load report for details.</value>
</data>
<data name="ConsolePanel_ProjectCheckFindingsTitle" xml:space="preserve">
<value>Project Check Findings</value>
Expand Down Expand Up @@ -1023,13 +1068,61 @@ Do you wish to continue?</value>
<value>Publish Package</value>
</data>
<data name="Package_PublishConfirm_Message" xml:space="preserve">
<value>Publish package '{0}' to the remote registry?</value>
<value>Publish package '{0}' to the workshop as a new version?</value>
</data>
<data name="Package_PublishStaleConfirm_Message" xml:space="preserve">
<value>Publish '{0}'? Version {1} is installed but the latest is now {2} — publishing may overwrite newer work.</value>
</data>
<data name="Package_PublishUnreadableRecordConfirm_Message" xml:space="preserve">
<value>Publish '{0}'? Its install record (HISTORY.md) can't be read, so it may be behind the latest version.</value>
</data>
<data name="Package_InstallConfirm_Title" xml:space="preserve">
<value>Install Package</value>
</data>
<data name="Package_InstallConfirm_Message" xml:space="preserve">
<value>Install package '{0}' from the remote registry?</value>
<value>Install package '{0}' version {1} into '{2}'?</value>
</data>
<data name="Package_ReplaceConfirm_Title" xml:space="preserve">
<value>Replace Package</value>
</data>
<data name="Package_ReplaceConfirm_Message" xml:space="preserve">
<value>'{0}' already contains package '{1}' (version {2}). Installing version {3} replaces its contents, moving the current files to the trash. Continue?</value>
</data>
<data name="Package_ReplaceConfirm_MessageUnknownVersion" xml:space="preserve">
<value>'{0}' already contains package '{1}'. Installing version {2} replaces its contents, moving the current files to the trash. Continue?</value>
</data>
<data name="Package_DeleteConfirm_Title" xml:space="preserve">
<value>Delete Package Version</value>
</data>
<data name="Package_DeleteConfirm_Message" xml:space="preserve">
<value>Delete version {0} of package '{1}' from the workshop? Its content will be permanently removed and cannot be recovered.</value>
</data>
<data name="Package_DeleteConfirm_MessageWithAliases" xml:space="preserve">
<value>Delete version {0} of package '{1}' from the workshop? Its content will be permanently removed and cannot be recovered. These aliases will be left pointing at the deleted version: {2}.</value>
</data>
<data name="Package_UnpublishConfirm_Title" xml:space="preserve">
<value>Unpublish Package</value>
</data>
<data name="Package_UnpublishConfirm_Message" xml:space="preserve">
<value>Unpublish package '{0}' from the workshop? The package and all its versions will be permanently removed and cannot be recovered.</value>
</data>
<data name="Page_PublishConfirm_Title" xml:space="preserve">
<value>Publish Page</value>
</data>
<data name="Page_PublishConfirm_Message" xml:space="preserve">
<value>Publish this folder as a page at '{0}'? It will be served publicly on the workshop.</value>
</data>
<data name="Page_UnpublishConfirm_Title" xml:space="preserve">
<value>Unpublish Page</value>
</data>
<data name="Page_UnpublishConfirm_Message" xml:space="preserve">
<value>Unpublish the page at '{0}' from the workshop? It will no longer be served.</value>
</data>
<data name="Workshop_PublishBlocked_Title" xml:space="preserve">
<value>Cannot Publish</value>
</data>
<data name="Workshop_PublishBlocked_Message" xml:space="preserve">
<value>No Author is set. Add one on the Settings page, then try again.</value>
</data>
<data name="DocumentEditor_SpreadsheetEditor" xml:space="preserve">
<value>Spreadsheet Editor</value>
Expand Down Expand Up @@ -1093,4 +1186,4 @@ Do you wish to continue?</value>
<data name="DocumentTab_ReopenWith" xml:space="preserve">
<value>Reopen with...</value>
</data>
</root>
</root>
5 changes: 5 additions & 0 deletions Source/Core/Celbridge.FileSystem/Services/LocalFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ private static FileSystemAttributes MapToPortable(System.IO.FileAttributes nativ
portable |= FileSystemAttributes.ReadOnly;
}

if ((native & System.IO.FileAttributes.ReparsePoint) != 0)
{
portable |= FileSystemAttributes.ReparsePoint;
}

return portable;
}
}
21 changes: 20 additions & 1 deletion Source/Core/Celbridge.Foundation/Core/ResourceKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ public bool IsDescendantOf(ResourceKey folderKey)
}

/// <summary>
/// Returns a new ResourceKey that is the combination of the current key and the specified segment.
/// Returns a new ResourceKey that is the combination of the current key and exactly one segment.
/// The root is preserved; the segment is appended to the path.
/// </summary>
public ResourceKey Combine(string segment)
Expand All @@ -261,6 +261,25 @@ public ResourceKey Combine(string segment)
return new ResourceKey(_root, combinedPath);
}

/// <summary>
/// Returns a new ResourceKey formed by appending a relative path to the current key.
/// The path is forward-slash separated and may span multiple segments, each validated
/// as Combine validates a single segment. The root is preserved.
/// </summary>
public ResourceKey CombinePath(string relativePath)
{
ArgumentException.ThrowIfNullOrEmpty(relativePath);

var key = this;
var segments = relativePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
key = key.Combine(segment);
}

return key;
}

/// <summary>
/// Returns true if the string represents a valid resource key segment.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace Celbridge.Credentials;
public static class CredentialConstants
{
/// <summary>
/// The prefix of a well-formed Workshop Application Key, shaped like
/// The prefix of a well-formed Workshop Key, shaped like
/// "kpf_(prefix)_(secret)". The prefix identifies the key and is not secret.
/// </summary>
public const string ApplicationKeyPrefix = "kpf_";
public const string WorkshopKeyPrefix = "kpf_";
}
47 changes: 21 additions & 26 deletions Source/Core/Celbridge.Foundation/Credentials/ICredentialService.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,19 @@
namespace Celbridge.Credentials;

/// <summary>
/// A Workshop server URL paired with the Application Key issued by that server.
/// The two values are stored and retrieved together because a key is only
/// meaningful against the server that issued it.
/// Summary of the stored Workshop Key, readable without decrypting it.
/// KeyHint is the identifying prefix of the stored key, or empty when the key
/// has no recognisable prefix or the stored entry is unreadable.
/// </summary>
public record WorkshopConnection(string WorkshopUrl, string ApplicationKey);
public record WorkshopKeySummary(bool IsStored, string KeyHint);

/// <summary>
/// Summary of the stored Workshop connection, readable without decrypting it.
/// KeyHint is the identifying prefix of the stored Application Key, or empty
/// when the key has no recognisable prefix or the stored entry is unreadable.
/// </summary>
public record WorkshopConnectionSummary(bool IsStored, string KeyHint);

/// <summary>
/// Application-scoped store for sensitive credentials, encrypted at rest.
/// Stored values are retrievable only by host-side services through this typed
/// API and must never appear on agent-readable surfaces such as tool results,
/// log messages, the WebView, scripting APIs, or subprocess environments.
/// Application-scoped store for secret credentials, encrypted at rest. The store
/// is general purpose, with one typed accessor per credential. Stored values are
/// retrievable only by host-side services through this typed API and must never
/// appear on agent-readable surfaces such as tool results, log messages, the
/// WebView, scripting APIs, or subprocess environments. Only secrets belong here;
/// non-secret configuration belongs in settings.
/// </summary>
public interface ICredentialService
{
Expand All @@ -30,25 +25,25 @@ public interface ICredentialService
bool IsAvailable { get; }

/// <summary>
/// Gets a summary of the stored Workshop connection without decrypting it,
/// so display surfaces can identify the stored key. Reports a stored entry
/// even when it is corrupt, so callers can offer clear and replace.
/// Gets a summary of the stored Workshop Key without decrypting it, so
/// display surfaces can identify the stored key. Reports a stored entry even
/// when it is corrupt, so callers can offer clear and replace.
/// </summary>
Task<Result<WorkshopConnectionSummary>> GetWorkshopConnectionSummaryAsync();
Task<Result<WorkshopKeySummary>> GetWorkshopKeySummaryAsync();

/// <summary>
/// Gets the stored Workshop connection. Fails with an actionable message
/// when no connection is stored or the stored entry cannot be read.
/// Gets the stored Workshop Key. Fails with an actionable message when no
/// key is stored or the stored entry cannot be read.
/// </summary>
Task<Result<WorkshopConnection>> GetWorkshopConnectionAsync();
Task<Result<string>> GetWorkshopKeyAsync();

/// <summary>
/// Stores the Workshop connection, replacing any existing one.
/// Stores the Workshop Key, replacing any existing one.
/// </summary>
Task<Result> SetWorkshopConnectionAsync(WorkshopConnection connection);
Task<Result> SetWorkshopKeyAsync(string workshopKey);

/// <summary>
/// Removes the stored Workshop connection. Succeeds when no connection is stored.
/// Removes the stored Workshop Key. Succeeds when none is stored.
/// </summary>
Task<Result> ClearWorkshopConnectionAsync();
Task<Result> ClearWorkshopKeyAsync();
}
9 changes: 9 additions & 0 deletions Source/Core/Celbridge.Foundation/Dialog/DialogMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Celbridge.Dialog;

/// <summary>
/// Broadcast by IDialogService to deliver a scheduled automated answer to the
/// open modal dialog of the named kind. Kind identifies the target dialog;
/// Payload carries the answer data for that dialog. Used only by the debug-only
/// dialog test automation.
/// </summary>
public record DialogAnswerMessage(DialogKind Kind, string Payload);
5 changes: 5 additions & 0 deletions Source/Core/Celbridge.Foundation/Dialog/IDialogFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public interface IDialogFactory
/// </summary>
IInputTextDialog CreateInputTextDialog(string titleText, string messageText, string defaultText, Range selectionRange, IValidator validator, string? submitButtonKey = null);

/// <summary>
/// Create a Secret Input Dialog that masks the entered value.
/// </summary>
ISecretInputDialog CreateSecretInputDialog(string titleText, string headerText, string? submitButtonKey = null);

/// <summary>
/// Create an Add File Dialog.
/// </summary>
Expand Down
Loading
Loading