pikuri-workspace 0.0.3
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.
- checksums.yaml +7 -0
- data/README.md +50 -0
- data/lib/pikuri/tool/confirmer.rb +96 -0
- data/lib/pikuri/tool/edit.rb +196 -0
- data/lib/pikuri/tool/glob.rb +310 -0
- data/lib/pikuri/tool/grep.rb +338 -0
- data/lib/pikuri/tool/read.rb +254 -0
- data/lib/pikuri/tool/workspace.rb +150 -0
- data/lib/pikuri/tool/write.rb +170 -0
- data/lib/pikuri-workspace.rb +27 -0
- metadata +80 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
|
|
5
|
+
module Pikuri
|
|
6
|
+
class Tool
|
|
7
|
+
# Defines which paths the agent can see and write to. Subclass and
|
|
8
|
+
# implement {#cwd}, {#resolve_for_read}, and {#resolve_for_write}.
|
|
9
|
+
# Returned Pathnames are absolute, post-symlink-resolution.
|
|
10
|
+
#
|
|
11
|
+
# == Read-set vs. write-set
|
|
12
|
+
#
|
|
13
|
+
# The two resolve methods exist because a future workspace may admit
|
|
14
|
+
# paths for reading that it refuses for writing — e.g. a multi-root
|
|
15
|
+
# workspace where +~/.claude/skills+ is readable but only the project
|
|
16
|
+
# CWD is writable. For v1's {Cwd} the two methods are the same: read-
|
|
17
|
+
# set and write-set are both the CWD subtree.
|
|
18
|
+
#
|
|
19
|
+
# == Existence is not the workspace's concern
|
|
20
|
+
#
|
|
21
|
+
# +resolve_for_read('foo.rb')+ succeeds (returns a +Pathname+) even if
|
|
22
|
+
# +foo.rb+ doesn't exist; the caller ({Tool::Read}) errors with
|
|
23
|
+
# file-not-found when it tries to open it. +resolve_for_write+
|
|
24
|
+
# tolerates entirely non-existent paths (Write can create
|
|
25
|
+
# +lib/new/dir/foo.rb+ even when +lib/new/+ doesn't exist) — the
|
|
26
|
+
# caller is responsible for any +mkdir_p+ before writing. This split
|
|
27
|
+
# keeps the workspace narrowly responsible for *containment*, not for
|
|
28
|
+
# filesystem-state checks.
|
|
29
|
+
#
|
|
30
|
+
# == Future extensions (out of scope for v1)
|
|
31
|
+
#
|
|
32
|
+
# * Multi-root workspace: +~/.claude/skills+ readable, CWD writable.
|
|
33
|
+
# * +Tool::Workspace::ALLOW_ALL+: no restriction, planned for Docker /
|
|
34
|
+
# dev-container mode.
|
|
35
|
+
class Workspace
|
|
36
|
+
# Raised for any path that resolves outside the workspace. Recoverable
|
|
37
|
+
# at the tool layer — tools rescue this and emit +"Error: ..."+
|
|
38
|
+
# observations so the LLM can self-correct on the next turn.
|
|
39
|
+
class Error < StandardError; end
|
|
40
|
+
|
|
41
|
+
# Project anchor: where shells run, where modifications are made.
|
|
42
|
+
# For {Cwd} this is the constructor's +root:+ (after +realpath+);
|
|
43
|
+
# a future +ALLOW_ALL+ would return the user's invocation
|
|
44
|
+
# directory.
|
|
45
|
+
#
|
|
46
|
+
# @return [Pathname]
|
|
47
|
+
# @raise [NotImplementedError] in the abstract base
|
|
48
|
+
def cwd
|
|
49
|
+
raise NotImplementedError, "#{self.class}#cwd must be implemented"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Resolve a user-supplied path against the workspace's read-set.
|
|
53
|
+
# Returned Pathname is absolute and may not exist on disk; the
|
|
54
|
+
# caller validates existence separately.
|
|
55
|
+
#
|
|
56
|
+
# @param path [String] user-supplied path, absolute or relative
|
|
57
|
+
# @return [Pathname]
|
|
58
|
+
# @raise [Error] if the resolved path falls outside the read-set
|
|
59
|
+
# @raise [NotImplementedError] in the abstract base
|
|
60
|
+
def resolve_for_read(path)
|
|
61
|
+
raise NotImplementedError, "#{self.class}#resolve_for_read must be implemented"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Resolve a user-supplied path against the workspace's write-set.
|
|
65
|
+
# Same shape as {#resolve_for_read}; semantically distinct so a
|
|
66
|
+
# future workspace can permit reading paths it refuses to write.
|
|
67
|
+
#
|
|
68
|
+
# @param path [String]
|
|
69
|
+
# @return [Pathname]
|
|
70
|
+
# @raise [Error] if the resolved path falls outside the write-set
|
|
71
|
+
# @raise [NotImplementedError] in the abstract base
|
|
72
|
+
def resolve_for_write(path)
|
|
73
|
+
raise NotImplementedError, "#{self.class}#resolve_for_write must be implemented"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Locks read+write to a single subtree (the +root:+ passed at
|
|
77
|
+
# construction, with symlinks resolved). +cwd+ equals the root.
|
|
78
|
+
#
|
|
79
|
+
# == Containment algorithm
|
|
80
|
+
#
|
|
81
|
+
# +#resolve+ walks up the input path to its deepest existing
|
|
82
|
+
# ancestor, +realpath+'s that ancestor (resolving any symlinks in
|
|
83
|
+
# the existing portion), then verifies the resolved anchor is
|
|
84
|
+
# +@root+ or a descendant. Four cases the algorithm must handle:
|
|
85
|
+
#
|
|
86
|
+
# 1. +lib/foo.rb+ (exists) → +existing+ = full path, +anchor+ in
|
|
87
|
+
# root → returns the realpath'd file.
|
|
88
|
+
# 2. +lib/new/dir/foo.rb+ (intermediates missing) → walks up to
|
|
89
|
+
# +@root/lib+, +anchor+ in root → returns the intended new path
|
|
90
|
+
# (caller +mkdir_p+s the parent before writing).
|
|
91
|
+
# 3. +lib/../../etc/passwd+ (+..+ escape) → +cleanpath+ collapses
|
|
92
|
+
# +..+ syntactically, walks land outside +@root+ → +Error+.
|
|
93
|
+
# 4. +link/foo.rb+ where +link → /etc+ (symlink escape) → walks to
|
|
94
|
+
# +link+ (which exists), +realpath+ resolves through the
|
|
95
|
+
# symlink to +/etc+, outside +@root+ → +Error+.
|
|
96
|
+
#
|
|
97
|
+
# Pure lexical normalization (+cleanpath+ + prefix check) catches
|
|
98
|
+
# cases 1–3 but misses case 4. The walk-up step adds the
|
|
99
|
+
# +realpath+ pass on the existing prefix, closing that gap.
|
|
100
|
+
class Cwd < Workspace
|
|
101
|
+
# @param root [String, Pathname] absolute (or working-directory-
|
|
102
|
+
# relative) path to anchor the workspace at. +realpath+'d once
|
|
103
|
+
# so subsequent comparisons happen in canonical form.
|
|
104
|
+
# @raise [Errno::ENOENT] if +root+ does not exist; surfaces
|
|
105
|
+
# immediately so misconfigured callers fail loudly.
|
|
106
|
+
def initialize(root:)
|
|
107
|
+
@root = Pathname.new(root).realpath
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Pathname]
|
|
111
|
+
def cwd
|
|
112
|
+
@root
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @param path [String]
|
|
116
|
+
# @return [Pathname]
|
|
117
|
+
# @raise [Error]
|
|
118
|
+
def resolve_for_read(path)
|
|
119
|
+
resolve(path)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# @param path [String]
|
|
123
|
+
# @return [Pathname]
|
|
124
|
+
# @raise [Error]
|
|
125
|
+
def resolve_for_write(path)
|
|
126
|
+
resolve(path)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
# See class header for the algorithm rationale.
|
|
132
|
+
def resolve(path)
|
|
133
|
+
pn = Pathname.new(path)
|
|
134
|
+
pn = @root + pn unless pn.absolute?
|
|
135
|
+
cleaned = pn.cleanpath
|
|
136
|
+
|
|
137
|
+
existing = cleaned
|
|
138
|
+
existing = existing.parent until existing.exist? || existing.parent == existing
|
|
139
|
+
anchor = existing.realpath
|
|
140
|
+
|
|
141
|
+
unless anchor == @root || anchor.to_s.start_with?(@root.to_s + File::SEPARATOR)
|
|
142
|
+
raise Error, "path '#{path}' is outside the workspace '#{@root}'"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
anchor + cleaned.relative_path_from(existing)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Pikuri
|
|
6
|
+
class Tool
|
|
7
|
+
# The +write+ tool, expressed as a {Tool} subclass: instantiating
|
|
8
|
+
# +Tool::Write.new(workspace: ws, confirmer: c)+ produces a tool whose
|
|
9
|
+
# {Tool#to_ruby_llm_tool} wiring is identical to any bundled tool's,
|
|
10
|
+
# so ruby_llm sees nothing special about it. Same shape as
|
|
11
|
+
# {Tool::SubAgent} and {Tool::Read} — workspace and confirmer are
|
|
12
|
+
# captured by the +execute+ closure at construction.
|
|
13
|
+
#
|
|
14
|
+
# == Policy
|
|
15
|
+
#
|
|
16
|
+
# Three branches based on the on-disk state of +path+:
|
|
17
|
+
#
|
|
18
|
+
# 1. *New file* — write, no prompt.
|
|
19
|
+
# 2. *Existing file, identical content* — return an +"Error: ..."+
|
|
20
|
+
# no-op observation *before* invoking the confirmer; don't ask the
|
|
21
|
+
# user to approve a write that wouldn't change the file. Comparison
|
|
22
|
+
# is byte-strict, in BINARY encoding (trailing-newline-only
|
|
23
|
+
# differences trigger the confirm path; encoding tags can't make
|
|
24
|
+
# equal bytes compare unequal).
|
|
25
|
+
# 3. *Existing file, content differs* — confirm with
|
|
26
|
+
# +"OK to overwrite <path>: <old> → <new> bytes?"+ via {Confirmer};
|
|
27
|
+
# on yes, write. On no, return a decline-Error observation.
|
|
28
|
+
#
|
|
29
|
+
# == Why ask-on-overwrite (Edit doesn't)
|
|
30
|
+
#
|
|
31
|
+
# Edit's +old_string+ argument is an implicit read-check: the model
|
|
32
|
+
# can't write a correct +old_string+ without having read the file, so
|
|
33
|
+
# blast radius is bounded by what the model actually knows about file
|
|
34
|
+
# state. Write has no such check, so a hallucinated 500-line +content+
|
|
35
|
+
# could clobber unread work. The confirmation prompt guards exactly
|
|
36
|
+
# the gap Edit's argument shape already covers.
|
|
37
|
+
#
|
|
38
|
+
# == Side effects
|
|
39
|
+
#
|
|
40
|
+
# Parent directories are created (+FileUtils.mkdir_p+) before the
|
|
41
|
+
# write — matches the +git add lib/new/dir/foo.rb+ mental model and
|
|
42
|
+
# mirrors opencode's and pi's behavior. Edge case: +mkdir_p+ succeeds
|
|
43
|
+
# but the write fails; an empty directory is left behind. Accepted
|
|
44
|
+
# for v1 — users have version control.
|
|
45
|
+
#
|
|
46
|
+
# No atomic temp-file+rename. Plain +File.write+, same as opencode and
|
|
47
|
+
# pi. The crash-safety story is "the user has git".
|
|
48
|
+
class Write < Tool
|
|
49
|
+
# Description shown to the LLM. Follows the opencode-shape (summary
|
|
50
|
+
# + +Usage:+ bullets) prescribed by the project's tool-description
|
|
51
|
+
# convention. Per-parameter constraints live in the parameter
|
|
52
|
+
# descriptions; the +Usage:+ bullets are for "when do I pick this?
|
|
53
|
+
# how does it chain with other tools?".
|
|
54
|
+
#
|
|
55
|
+
# @return [String]
|
|
56
|
+
DESCRIPTION = <<~DESC
|
|
57
|
+
Write a file to the workspace, creating parent directories as needed.
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
- Use for new files or full-file rewrites; for partial changes use `edit` instead.
|
|
61
|
+
- Overwriting an existing file requires user confirmation; identical content is rejected as a no-op error — if you see that error, re-read the file rather than trying again.
|
|
62
|
+
- Parent directories are created automatically (mkdir -p).
|
|
63
|
+
- Writes the exact bytes supplied: no trailing-newline normalization, no encoding conversion.
|
|
64
|
+
- Paths outside the workspace are refused.
|
|
65
|
+
DESC
|
|
66
|
+
|
|
67
|
+
# @param workspace [Tool::Workspace] captured for path resolution;
|
|
68
|
+
# all writes route through +workspace.resolve_for_write+.
|
|
69
|
+
# @param confirmer [Tool::Confirmer] consulted before any overwrite
|
|
70
|
+
# of an existing file with non-identical content.
|
|
71
|
+
# @return [Write]
|
|
72
|
+
def initialize(workspace:, confirmer:)
|
|
73
|
+
super(
|
|
74
|
+
name: 'write',
|
|
75
|
+
description: DESCRIPTION,
|
|
76
|
+
parameters: Parameters.build { |p|
|
|
77
|
+
p.required_string :path,
|
|
78
|
+
'Path to the file to write. Relative paths ' \
|
|
79
|
+
'resolve against the workspace root, e.g. ' \
|
|
80
|
+
'"lib/foo.rb".'
|
|
81
|
+
p.required_string :content,
|
|
82
|
+
'Full contents to write to the file, e.g. ' \
|
|
83
|
+
'"class Foo\nend\n".'
|
|
84
|
+
},
|
|
85
|
+
execute: ->(path:, content:) {
|
|
86
|
+
Write.write(workspace: workspace, confirmer: confirmer, path: path, content: content)
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Resolve +path+ against +workspace+, apply the three-branch policy
|
|
92
|
+
# (new / identical / differs), and return either a success
|
|
93
|
+
# observation or an +"Error: ..."+ observation.
|
|
94
|
+
#
|
|
95
|
+
# @param workspace [Tool::Workspace]
|
|
96
|
+
# @param confirmer [Tool::Confirmer]
|
|
97
|
+
# @param path [String] raw path as supplied by the LLM
|
|
98
|
+
# @param content [String] bytes to write
|
|
99
|
+
# @return [String] tool observation
|
|
100
|
+
def self.write(workspace:, confirmer:, path:, content:)
|
|
101
|
+
resolved = workspace.resolve_for_write(path)
|
|
102
|
+
|
|
103
|
+
if resolved.exist?
|
|
104
|
+
return "Error: #{path} is a directory" if resolved.directory?
|
|
105
|
+
|
|
106
|
+
existing = read_for_compare(resolved, path)
|
|
107
|
+
|
|
108
|
+
if existing == content.b
|
|
109
|
+
return "Error: #{path} already contains exactly this content — " \
|
|
110
|
+
'no write needed. If you intended a change, re-read the ' \
|
|
111
|
+
'file and try again.'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
prompt = "OK to overwrite #{path}: #{existing.bytesize} → #{content.bytesize} bytes? (y/n)"
|
|
115
|
+
return "Error: user declined the write to #{path}." unless confirmer.confirm?(prompt: prompt)
|
|
116
|
+
|
|
117
|
+
write_bytes(resolved, content)
|
|
118
|
+
"Updated #{path} (#{existing.bytesize} → #{content.bytesize} bytes)"
|
|
119
|
+
else
|
|
120
|
+
write_bytes(resolved, content)
|
|
121
|
+
"Created #{path} (#{content.bytesize} bytes)"
|
|
122
|
+
end
|
|
123
|
+
rescue Tool::Workspace::Error, Error => e
|
|
124
|
+
"Error: #{e.message}"
|
|
125
|
+
rescue Errno::EACCES => e
|
|
126
|
+
"Error: cannot write #{path}: #{e.message}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Internal-only signal for LLM-actionable preconditions that Write
|
|
130
|
+
# detects before any filesystem mutation — today, "existing file is
|
|
131
|
+
# unreadable so the identical-content check can't run". Mirrors
|
|
132
|
+
# {Tool::Workspace::Error} in shape: caught by {.write}'s outer
|
|
133
|
+
# rescue and rendered as a +"Error: ..."+ observation. The class
|
|
134
|
+
# is +private_constant+ to keep it from leaking out as a public
|
|
135
|
+
# exception type; callers should never +rescue+ this directly.
|
|
136
|
+
class Error < StandardError; end
|
|
137
|
+
private_constant :Error
|
|
138
|
+
|
|
139
|
+
# Read the existing file in BINARY mode so the equality check is
|
|
140
|
+
# purely byte-wise. Raises {Error} on +Errno::EACCES+ so {.write}'s
|
|
141
|
+
# outer rescue can render a specific "cannot read for overwrite
|
|
142
|
+
# check" message — without this conversion the bottom +Errno::EACCES+
|
|
143
|
+
# rescue would mislabel the read-side problem as "cannot write".
|
|
144
|
+
#
|
|
145
|
+
# @param resolved [Pathname]
|
|
146
|
+
# @param path [String] original path for the error message
|
|
147
|
+
# @return [String]
|
|
148
|
+
# @raise [Error] when the file exists but isn't readable
|
|
149
|
+
def self.read_for_compare(resolved, path)
|
|
150
|
+
resolved.binread
|
|
151
|
+
rescue Errno::EACCES => e
|
|
152
|
+
raise Error, "cannot read #{path} for overwrite check: #{e.message}"
|
|
153
|
+
end
|
|
154
|
+
private_class_method :read_for_compare
|
|
155
|
+
|
|
156
|
+
# +mkdir_p+ the parent, then write. Split out so both the create-
|
|
157
|
+
# and overwrite-branches can reuse it without duplicating the
|
|
158
|
+
# +mkdir_p+ call.
|
|
159
|
+
#
|
|
160
|
+
# @param resolved [Pathname]
|
|
161
|
+
# @param content [String]
|
|
162
|
+
# @return [void]
|
|
163
|
+
def self.write_bytes(resolved, content)
|
|
164
|
+
FileUtils.mkdir_p(resolved.dirname)
|
|
165
|
+
resolved.write(content)
|
|
166
|
+
end
|
|
167
|
+
private_class_method :write_bytes
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pikuri-core'
|
|
4
|
+
|
|
5
|
+
# Entry file for the pikuri-workspace gem. After
|
|
6
|
+
# +require 'pikuri-workspace'+, +Pikuri::Tool::Workspace+,
|
|
7
|
+
# +Pikuri::Tool::Confirmer+, and the five file tools (+Read+,
|
|
8
|
+
# +Write+, +Edit+, +Grep+, +Glob+) are all defined.
|
|
9
|
+
#
|
|
10
|
+
# The Zeitwerk loader is mounted under +Pikuri::Tool+ (rather than
|
|
11
|
+
# rooted at this gem's +lib/+) because +Pikuri::Tool+ is a class
|
|
12
|
+
# owned by pikuri-core; without the explicit +namespace:+ argument,
|
|
13
|
+
# Zeitwerk would try to redefine +Pikuri::Tool+ as a module from the
|
|
14
|
+
# implicit-namespace inference, which would conflict. Mounting at
|
|
15
|
+
# the existing class side-steps the conflict and lets our nested
|
|
16
|
+
# files (+lib/pikuri/tool/workspace.rb+ → +Pikuri::Tool::Workspace+,
|
|
17
|
+
# etc.) autoload naturally.
|
|
18
|
+
module Pikuri
|
|
19
|
+
class Tool
|
|
20
|
+
LOADER = Zeitwerk::Loader.new
|
|
21
|
+
LOADER.tag = 'pikuri-workspace'
|
|
22
|
+
LOADER.push_dir(File.expand_path('pikuri/tool', __dir__), namespace: Pikuri::Tool)
|
|
23
|
+
LOADER.ignore(File.expand_path('pikuri-workspace.rb', __dir__))
|
|
24
|
+
LOADER.setup
|
|
25
|
+
LOADER.eager_load
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pikuri-workspace
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Martin Vysny
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: pikuri-core
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.0.3
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.0.3
|
|
27
|
+
description: |
|
|
28
|
+
pikuri-workspace adds "operate on a directory tree" to pikuri-core
|
|
29
|
+
agents: the +Pikuri::Tool::Workspace+ seam (abstract base +
|
|
30
|
+
+Workspace::Cwd+) that scopes filesystem access to a chosen root,
|
|
31
|
+
the +Pikuri::Tool::Confirmer+ seam (+AUTO_APPROVE+ + +TERMINAL+)
|
|
32
|
+
for user-state mutations, and five tools wired to those seams:
|
|
33
|
+
+Pikuri::Tool::Read+, +Pikuri::Tool::Write+, +Pikuri::Tool::Edit+,
|
|
34
|
+
+Pikuri::Tool::Grep+, and +Pikuri::Tool::Glob+. Self-contained —
|
|
35
|
+
no shell execution; +Pikuri::Tool::Bash+ ships in pikuri-code on
|
|
36
|
+
top of these.
|
|
37
|
+
email:
|
|
38
|
+
- martin@vysny.me
|
|
39
|
+
executables: []
|
|
40
|
+
extensions: []
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
files:
|
|
43
|
+
- README.md
|
|
44
|
+
- lib/pikuri-workspace.rb
|
|
45
|
+
- lib/pikuri/tool/confirmer.rb
|
|
46
|
+
- lib/pikuri/tool/edit.rb
|
|
47
|
+
- lib/pikuri/tool/glob.rb
|
|
48
|
+
- lib/pikuri/tool/grep.rb
|
|
49
|
+
- lib/pikuri/tool/read.rb
|
|
50
|
+
- lib/pikuri/tool/workspace.rb
|
|
51
|
+
- lib/pikuri/tool/write.rb
|
|
52
|
+
homepage: https://codeberg.org/mvysny/pikuri
|
|
53
|
+
licenses:
|
|
54
|
+
- MIT
|
|
55
|
+
metadata:
|
|
56
|
+
source_code_uri: https://codeberg.org/mvysny/pikuri/src/branch/master
|
|
57
|
+
changelog_uri: https://codeberg.org/mvysny/pikuri/src/branch/master/CHANGELOG.md
|
|
58
|
+
bug_tracker_uri: https://codeberg.org/mvysny/pikuri/issues
|
|
59
|
+
rubygems_mfa_required: 'true'
|
|
60
|
+
post_install_message:
|
|
61
|
+
rdoc_options: []
|
|
62
|
+
require_paths:
|
|
63
|
+
- lib
|
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.3'
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
requirements: []
|
|
75
|
+
rubygems_version: 3.5.22
|
|
76
|
+
signing_key:
|
|
77
|
+
specification_version: 4
|
|
78
|
+
summary: Filesystem tools (Read/Write/Edit/Grep/Glob) + Workspace + Confirmer seams
|
|
79
|
+
for pikuri.
|
|
80
|
+
test_files: []
|